Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.camera.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorSet;
     21 import android.animation.TimeInterpolator;
     22 import android.animation.ValueAnimator;
     23 import android.app.Activity;
     24 import android.content.Context;
     25 import android.content.res.Configuration;
     26 import android.graphics.Canvas;
     27 import android.graphics.Rect;
     28 import android.graphics.RectF;
     29 import android.net.Uri;
     30 import android.os.Handler;
     31 import android.util.AttributeSet;
     32 import android.util.DisplayMetrics;
     33 import android.util.Log;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.view.animation.DecelerateInterpolator;
     38 import android.widget.Scroller;
     39 
     40 import com.android.camera.CameraActivity;
     41 import com.android.camera.data.LocalData;
     42 import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback;
     43 import com.android.camera.ui.FilmstripBottomControls.BottomControlsListener;
     44 import com.android.camera.util.CameraUtil;
     45 import com.android.camera.util.PhotoSphereHelper.PanoramaViewHelper;
     46 import com.android.camera.util.UsageStatistics;
     47 import com.android.camera2.R;
     48 
     49 import java.util.Arrays;
     50 
     51 public class FilmStripView extends ViewGroup implements BottomControlsListener {
     52     private static final String TAG = "CAM_FilmStripView";
     53 
     54     private static final int BUFFER_SIZE = 5;
     55     private static final int GEOMETRY_ADJUST_TIME_MS = 400;
     56     private static final int SNAP_IN_CENTER_TIME_MS = 600;
     57     private static final float FLING_COASTING_DURATION_S = 0.05f;
     58     private static final int ZOOM_ANIMATION_DURATION_MS = 200;
     59     private static final int CAMERA_PREVIEW_SWIPE_THRESHOLD = 300;
     60     private static final float FILM_STRIP_SCALE = 0.5f;
     61     private static final float FULL_SCREEN_SCALE = 1f;
     62 
     63     private static final float TOLERANCE = 0.1f;
     64     // Only check for intercepting touch events within first 500ms
     65     private static final int SWIPE_TIME_OUT = 500;
     66     private static final int DECELERATION_FACTOR = 4;
     67 
     68     private CameraActivity mActivity;
     69     private FilmStripGestureRecognizer mGestureRecognizer;
     70     private DataAdapter mDataAdapter;
     71     private int mViewGap;
     72     private final Rect mDrawArea = new Rect();
     73 
     74     private final int mCurrentItem = (BUFFER_SIZE - 1) / 2;
     75     private float mScale;
     76     private MyController mController;
     77     private int mCenterX = -1;
     78     private ViewItem[] mViewItem = new ViewItem[BUFFER_SIZE];
     79 
     80     private Listener mListener;
     81     private ZoomView mZoomView = null;
     82 
     83     private MotionEvent mDown;
     84     private boolean mCheckToIntercept = true;
     85     private View mCameraView;
     86     private int mSlop;
     87     private TimeInterpolator mViewAnimInterpolator;
     88 
     89     private FilmstripBottomControls mBottomControls;
     90     private PanoramaViewHelper mPanoramaViewHelper;
     91     private long mLastItemId = -1;
     92 
     93     // This is true if and only if the user is scrolling,
     94     private boolean mIsUserScrolling;
     95     private int mDataIdOnUserScrolling;
     96     private ValueAnimator.AnimatorUpdateListener mViewItemUpdateListener;
     97     private float mOverScaleFactor = 1f;
     98 
     99     private int mLastTotalNumber = 0;
    100 
    101     /**
    102      * Common interface for all images in the filmstrip.
    103      */
    104     public interface ImageData {
    105 
    106         /**
    107          * Interface that is used to tell the caller whether an image is a photo
    108          * sphere.
    109          */
    110         public static interface PanoramaSupportCallback {
    111             /**
    112              * Called then photo sphere info has been loaded.
    113              *
    114              * @param isPanorama whether the image is a valid photo sphere
    115              * @param isPanorama360 whether the photo sphere is a full 360
    116              *            degree horizontal panorama
    117              */
    118             void panoramaInfoAvailable(boolean isPanorama,
    119                     boolean isPanorama360);
    120         }
    121 
    122         // View types.
    123         public static final int VIEW_TYPE_NONE = 0;
    124         public static final int VIEW_TYPE_STICKY = 1;
    125         public static final int VIEW_TYPE_REMOVABLE = 2;
    126 
    127         // Actions allowed to be performed on the image data.
    128         // The actions are defined bit-wise so we can use bit operations like
    129         // | and &.
    130         public static final int ACTION_NONE = 0;
    131         public static final int ACTION_PROMOTE = 1;
    132         public static final int ACTION_DEMOTE = (1 << 1);
    133         /**
    134          * For image data that supports zoom, it should also provide a valid
    135          * content uri.
    136          */
    137         public static final int ACTION_ZOOM = (1 << 2);
    138 
    139         /**
    140          * SIZE_FULL can be returned by {@link ImageData#getWidth()} and
    141          * {@link ImageData#getHeight()}. When SIZE_FULL is returned for
    142          * width/height, it means the the width or height will be disregarded
    143          * when deciding the view size of this ImageData, just use full screen
    144          * size.
    145          */
    146         public static final int SIZE_FULL = -2;
    147 
    148         /**
    149          * Returns the width of the image before orientation applied.
    150          * The final layout of the view returned by
    151          * {@link DataAdapter#getView(android.app.Activity, int)} will
    152          * preserve the aspect ratio of
    153          * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
    154          * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
    155          */
    156         public int getWidth();
    157 
    158         /**
    159          * Returns the height of the image before orientation applied.
    160          * The final layout of the view returned by
    161          * {@link DataAdapter#getView(android.app.Activity, int)} will
    162          * preserve the aspect ratio of
    163          * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and
    164          * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}.
    165          */
    166         public int getHeight();
    167 
    168         /**
    169          * Returns the orientation of the image.
    170          */
    171         public int getOrientation();
    172 
    173         /** Returns the image data type */
    174         public int getViewType();
    175 
    176         /**
    177          * Returns the coordinates of this item.
    178          *
    179          * @return A 2-element array containing {latitude, longitude}, or null,
    180          *         if no position is known for this item.
    181          */
    182         public double[] getLatLong();
    183 
    184         /**
    185          * Checks if the UI action is supported.
    186          *
    187          * @param action The UI actions to check.
    188          * @return {@code false} if at least one of the actions is not
    189          *         supported. {@code true} otherwise.
    190          */
    191         public boolean isUIActionSupported(int action);
    192 
    193         /**
    194          * Gives the data a hint when its view is going to be displayed.
    195          * {@code FilmStripView} should always call this function before showing
    196          * its corresponding view every time.
    197          */
    198         public void prepare();
    199 
    200         /**
    201          * Gives the data a hint when its view is going to be removed from the
    202          * view hierarchy. {@code FilmStripView} should always call this
    203          * function after its corresponding view is removed from the view
    204          * hierarchy.
    205          */
    206         public void recycle();
    207 
    208         /**
    209          * Asynchronously checks if the image is a photo sphere. Notified the
    210          * callback when the results are available.
    211          */
    212         public void isPhotoSphere(Context context, PanoramaSupportCallback callback);
    213 
    214         /**
    215          * If the item is a valid photo sphere panorama, this method will launch
    216          * the viewer.
    217          */
    218         public void viewPhotoSphere(PanoramaViewHelper helper);
    219 
    220         /** Whether this item is a photo. */
    221         public boolean isPhoto();
    222 
    223         /**
    224          * Returns the content URI of this data item.
    225          *
    226          * @return {@code Uri.EMPTY} if not valid.
    227          */
    228         public Uri getContentUri();
    229     }
    230 
    231     /**
    232      * An interfaces which defines the interactions between the
    233      * {@link ImageData} and the {@link FilmStripView}.
    234      */
    235     public interface DataAdapter {
    236         /**
    237          * An interface which defines the update report used to return to the
    238          * {@link com.android.camera.ui.FilmStripView.Listener}.
    239          */
    240         public interface UpdateReporter {
    241             /** Checks if the data of dataID is removed. */
    242             public boolean isDataRemoved(int dataID);
    243 
    244             /** Checks if the data of dataID is updated. */
    245             public boolean isDataUpdated(int dataID);
    246         }
    247 
    248         /**
    249          * An interface which defines the listener for data events over
    250          * {@link ImageData}. Usually {@link FilmStripView} itself.
    251          */
    252         public interface Listener {
    253             // Called when the whole data loading is done. No any assumption
    254             // on previous data.
    255             public void onDataLoaded();
    256 
    257             // Only some of the data is changed. The listener should check
    258             // if any thing needs to be updated.
    259             public void onDataUpdated(UpdateReporter reporter);
    260 
    261             public void onDataInserted(int dataID, ImageData data);
    262 
    263             public void onDataRemoved(int dataID, ImageData data);
    264         }
    265 
    266         /** Returns the total number of image data */
    267         public int getTotalNumber();
    268 
    269         /**
    270          * Returns the view to visually present the image data.
    271          *
    272          * @param activity The {@link Activity} context to create the view.
    273          * @param dataID The ID of the image data to be presented.
    274          * @return The view representing the image data. Null if unavailable or
    275          *         the {@code dataID} is out of range.
    276          */
    277         public View getView(Activity activity, int dataID);
    278 
    279         /**
    280          * Returns the {@link ImageData} specified by the ID.
    281          *
    282          * @param dataID The ID of the {@link ImageData}.
    283          * @return The specified {@link ImageData}. Null if not available.
    284          */
    285         public ImageData getImageData(int dataID);
    286 
    287         /**
    288          * Suggests the data adapter the maximum possible size of the layout so
    289          * the {@link DataAdapter} can optimize the view returned for the
    290          * {@link ImageData}.
    291          *
    292          * @param w Maximum width.
    293          * @param h Maximum height.
    294          */
    295         public void suggestViewSizeBound(int w, int h);
    296 
    297         /**
    298          * Sets the listener for data events over the ImageData.
    299          *
    300          * @param listener The listener to use.
    301          */
    302         public void setListener(Listener listener);
    303 
    304         /**
    305          * Returns {@code true} if the view of the data can be moved by swipe
    306          * gesture when in full-screen.
    307          *
    308          * @param dataID The ID of the data.
    309          * @return {@code true} if the view can be moved, {@code false}
    310          *         otherwise.
    311          */
    312         public boolean canSwipeInFullScreen(int dataID);
    313     }
    314 
    315     /**
    316      * An interface which defines the FilmStripView UI action listener.
    317      */
    318     public interface Listener {
    319         /**
    320          * Callback when the data is promoted.
    321          *
    322          * @param dataID The ID of the promoted data.
    323          */
    324         public void onDataPromoted(int dataID);
    325 
    326         /**
    327          * Callback when the data is demoted.
    328          *
    329          * @param dataID The ID of the demoted data.
    330          */
    331         public void onDataDemoted(int dataID);
    332 
    333         /**
    334          * The callback when the item enters/leaves full-screen. TODO: Call this
    335          * function actually.
    336          *
    337          * @param dataID The ID of the image data.
    338          * @param fullScreen {@code true} if the data is entering full-screen.
    339          *            {@code false} otherwise.
    340          */
    341         public void onDataFullScreenChange(int dataID, boolean fullScreen);
    342 
    343         /**
    344          * Called by {@link reload}.
    345          */
    346         public void onReload();
    347 
    348         /**
    349          * Called by {@link checkCurrentDataCentered} when the
    350          * data is centered in the film strip.
    351          *
    352          * @param dataID the ID of the local data
    353          */
    354         public void onCurrentDataCentered(int dataID);
    355 
    356         /**
    357          * Called by {@link checkCurrentDataCentered} when the
    358          * data is off centered in the film strip.
    359          *
    360          * @param dataID the ID of the local data
    361          */
    362         public void onCurrentDataOffCentered(int dataID);
    363 
    364         /**
    365          * The callback when the item is centered/off-centered.
    366          *
    367          * @param dataID The ID of the image data.
    368          * @param focused {@code true} if the data is focused.
    369          *            {@code false} otherwise.
    370          */
    371         public void onDataFocusChanged(int dataID, boolean focused);
    372 
    373         /**
    374          * Toggles the visibility of the ActionBar.
    375          *
    376          * @param dataID The ID of the image data.
    377          */
    378         public void onToggleSystemDecorsVisibility(int dataID);
    379 
    380         /**
    381          * Sets the visibility of system decors, including action bar and nav bar
    382          * @param visible The visibility of the system decors
    383          */
    384         public void setSystemDecorsVisibility(boolean visible);
    385     }
    386 
    387     /**
    388      * An interface which defines the controller of {@link FilmStripView}.
    389      */
    390     public interface Controller {
    391         public boolean isScaling();
    392 
    393         public void scroll(float deltaX);
    394 
    395         public void fling(float velocity);
    396 
    397         public void flingInsideZoomView (float velocityX, float velocityY);
    398 
    399         public void scrollToPosition(int position, int duration, boolean interruptible);
    400 
    401         public boolean goToNextItem();
    402 
    403         public boolean stopScrolling(boolean forced);
    404 
    405         public boolean isScrolling();
    406 
    407         public void goToFirstItem();
    408 
    409         public void goToFilmStrip();
    410 
    411         public void goToFullScreen();
    412     }
    413 
    414     /**
    415      * A helper class to tract and calculate the view coordination.
    416      */
    417     private static class ViewItem {
    418         private int mDataId;
    419         /** The position of the left of the view in the whole filmstrip. */
    420         private int mLeftPosition;
    421         private View mView;
    422         private RectF mViewArea;
    423 
    424         private ValueAnimator mTranslationXAnimator;
    425 
    426         /**
    427          * Constructor.
    428          *
    429          * @param id The id of the data from {@link DataAdapter}.
    430          * @param v The {@code View} representing the data.
    431          */
    432         public ViewItem(
    433                 int id, View v, ValueAnimator.AnimatorUpdateListener listener) {
    434             v.setPivotX(0f);
    435             v.setPivotY(0f);
    436             mDataId = id;
    437             mView = v;
    438             mLeftPosition = -1;
    439             mViewArea = new RectF();
    440             mTranslationXAnimator = new ValueAnimator();
    441             mTranslationXAnimator.addUpdateListener(listener);
    442         }
    443 
    444         /** Returns the data id from {@link DataAdapter}. */
    445         public int getId() {
    446             return mDataId;
    447         }
    448 
    449         /** Sets the data id from {@link DataAdapter}. */
    450         public void setId(int id) {
    451             mDataId = id;
    452         }
    453 
    454         /** Sets the left position of the view in the whole filmstrip. */
    455         public void setLeftPosition(int pos) {
    456             mLeftPosition = pos;
    457         }
    458 
    459         /** Returns the left position of the view in the whole filmstrip. */
    460         public int getLeftPosition() {
    461             return mLeftPosition;
    462         }
    463 
    464         /** Returns the translation of Y regarding the view scale. */
    465         public float getScaledTranslationY(float scale) {
    466             return mView.getTranslationY() / scale;
    467         }
    468 
    469         /** Returns the translation of X regarding the view scale. */
    470         public float getScaledTranslationX(float scale) {
    471             return mView.getTranslationX() / scale;
    472         }
    473 
    474         /**
    475          * The horizontal location of this view relative to its left position.
    476          * This position is post-layout, in addition to wherever the object's
    477          * layout placed it.
    478          *
    479          * @return The horizontal position of this view relative to its left position, in pixels.
    480          */
    481         public float getTranslationX() {
    482             return mView.getTranslationX();
    483         }
    484 
    485         /**
    486          * The vertical location of this view relative to its top position.
    487          * This position is post-layout, in addition to wherever the object's
    488          * layout placed it.
    489          *
    490          * @return The vertical position of this view relative to its top position,
    491          * in pixels.
    492          */
    493         public float getTranslationY() {
    494             return mView.getTranslationY();
    495         }
    496 
    497         /** Sets the translation of Y regarding the view scale. */
    498         public void setTranslationY(float transY, float scale) {
    499             mView.setTranslationY(transY * scale);
    500         }
    501 
    502         /** Sets the translation of X regarding the view scale. */
    503         public void setTranslationX(float transX, float scale) {
    504             mView.setTranslationX(transX * scale);
    505         }
    506 
    507         public void animateTranslationX(
    508                 float targetX, long duration_ms, TimeInterpolator interpolator) {
    509             mTranslationXAnimator.setInterpolator(interpolator);
    510             mTranslationXAnimator.setDuration(duration_ms);
    511             mTranslationXAnimator.setFloatValues(mView.getTranslationX(), targetX);
    512             mTranslationXAnimator.start();
    513         }
    514 
    515         /** Adjusts the translation of X regarding the view scale. */
    516         public void translateXBy(float transX, float scale) {
    517             mView.setTranslationX(mView.getTranslationX() + transX * scale);
    518         }
    519 
    520         public int getCenterX() {
    521             return mLeftPosition + mView.getMeasuredWidth() / 2;
    522         }
    523 
    524         /** Gets the view representing the data. */
    525         public View getView() {
    526             return mView;
    527         }
    528 
    529         /**
    530          * The visual x position of this view, in pixels.
    531          */
    532         public float getX() {
    533             return mView.getX();
    534         }
    535 
    536         /**
    537          * The visual y position of this view, in pixels.
    538          */
    539         public float getY() {
    540             return mView.getY();
    541         }
    542 
    543         private void layoutAt(int left, int top) {
    544             mView.layout(left, top, left + mView.getMeasuredWidth(),
    545                     top + mView.getMeasuredHeight());
    546         }
    547 
    548         /**
    549          * The bounding rect of the view.
    550          */
    551         public RectF getViewRect() {
    552             RectF r = new RectF();
    553             r.left = mView.getX();
    554             r.top = mView.getY();
    555             r.right = r.left + mView.getWidth() * mView.getScaleX();
    556             r.bottom = r.top + mView.getHeight() * mView.getScaleY();
    557             return r;
    558         }
    559 
    560         /**
    561          * Layouts the view in the area assuming the center of the area is at a
    562          * specific point of the whole filmstrip.
    563          *
    564          * @param drawArea The area when filmstrip will show in.
    565          * @param refCenter The absolute X coordination in the whole filmstrip
    566          *            of the center of {@code drawArea}.
    567          * @param scale The current scale of the filmstrip.
    568          */
    569         public void layoutIn(Rect drawArea, int refCenter, float scale) {
    570             final float translationX = (mTranslationXAnimator.isRunning() ?
    571                     (Float) mTranslationXAnimator.getAnimatedValue() : 0f);
    572             int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter + translationX) * scale);
    573             int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale);
    574             layoutAt(left, top);
    575             mView.setScaleX(scale);
    576             mView.setScaleY(scale);
    577 
    578             // update mViewArea for touch detection.
    579             int l = mView.getLeft();
    580             int t = mView.getTop();
    581             mViewArea.set(l, t,
    582                     l + mView.getMeasuredWidth() * scale,
    583                     t + mView.getMeasuredHeight() * scale);
    584         }
    585 
    586         /** Returns true if the point is in the view. */
    587         public boolean areaContains(float x, float y) {
    588             return mViewArea.contains(x, y);
    589         }
    590 
    591         /**
    592          * Return the width of the view.
    593          */
    594         public int getWidth() {
    595             return mView.getWidth();
    596         }
    597 
    598         public void copyGeometry(ViewItem item) {
    599             setLeftPosition(item.getLeftPosition());
    600             View v = item.getView();
    601             mView.setTranslationY(v.getTranslationY());
    602             mView.setTranslationX(v.getTranslationX());
    603         }
    604         /**
    605          * Apply a scale factor (i.e. {@param postScale}) on top of current scale at
    606          * pivot point ({@param focusX}, {@param focusY}). Visually it should be the
    607          * same as post concatenating current view's matrix with specified scale.
    608          */
    609         void postScale(float focusX, float focusY, float postScale, int viewportWidth,
    610                        int viewportHeight) {
    611             float transX = getTranslationX();
    612             float transY = getTranslationY();
    613             // Pivot point is top left of the view, so we need to translate
    614             // to scale around focus point
    615             transX -= (focusX - getX()) * (postScale - 1f);
    616             transY -= (focusY - getY()) * (postScale - 1f);
    617             float scaleX = mView.getScaleX() * postScale;
    618             float scaleY = mView.getScaleY() * postScale;
    619             updateTransform(transX, transY, scaleX, scaleY, viewportWidth,
    620                     viewportHeight);
    621         }
    622 
    623         void updateTransform(float transX, float transY, float scaleX, float scaleY,
    624                                     int viewportWidth, int viewportHeight) {
    625             float left = transX + mView.getLeft();
    626             float top = transY + mView.getTop();
    627             RectF r = ZoomView.adjustToFitInBounds(new RectF(left, top,
    628                     left + mView.getWidth() * scaleX,
    629                     top + mView.getHeight() * scaleY),
    630                     viewportWidth, viewportHeight);
    631             mView.setScaleX(scaleX);
    632             mView.setScaleY(scaleY);
    633             transX = r.left - mView.getLeft();
    634             transY = r.top - mView.getTop();
    635             mView.setTranslationX(transX);
    636             mView.setTranslationY(transY);
    637         }
    638 
    639         void resetTransform() {
    640             mView.setScaleX(FULL_SCREEN_SCALE);
    641             mView.setScaleY(FULL_SCREEN_SCALE);
    642             mView.setTranslationX(0f);
    643             mView.setTranslationY(0f);
    644         }
    645 
    646         @Override
    647         public String toString() {
    648             return "DataID = " + mDataId + "\n\t left = " + mLeftPosition
    649                     + "\n\t viewArea = " + mViewArea
    650                     + "\n\t centerX = " + getCenterX()
    651                     + "\n\t view MeasuredSize = "
    652                     + mView.getMeasuredWidth() + ',' + mView.getMeasuredHeight()
    653                     + "\n\t view Size = " + mView.getWidth() + ',' + mView.getHeight()
    654                     + "\n\t view scale = " + mView.getScaleX();
    655         }
    656     }
    657 
    658     public FilmStripView(Context context) {
    659         super(context);
    660         init((CameraActivity) context);
    661     }
    662 
    663     /** Constructor. */
    664     public FilmStripView(Context context, AttributeSet attrs) {
    665         super(context, attrs);
    666         init((CameraActivity) context);
    667     }
    668 
    669     /** Constructor. */
    670     public FilmStripView(Context context, AttributeSet attrs, int defStyle) {
    671         super(context, attrs, defStyle);
    672         init((CameraActivity) context);
    673     }
    674 
    675     private void init(CameraActivity cameraActivity) {
    676         setWillNotDraw(false);
    677         mActivity = cameraActivity;
    678         mScale = 1.0f;
    679         mDataIdOnUserScrolling = 0;
    680         mController = new MyController(cameraActivity);
    681         mViewAnimInterpolator = new DecelerateInterpolator();
    682         mZoomView = new ZoomView(cameraActivity);
    683         mZoomView.setVisibility(GONE);
    684         addView(mZoomView);
    685 
    686         mGestureRecognizer =
    687                 new FilmStripGestureRecognizer(cameraActivity, new MyGestureReceiver());
    688         mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop);
    689         mViewItemUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    690             @Override
    691             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    692                 invalidate();
    693             }
    694         };
    695         DisplayMetrics metrics = new DisplayMetrics();
    696         mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
    697         // Allow over scaling because on high density screens, pixels are too
    698         // tiny to clearly see the details at 1:1 zoom. We should not scale
    699         // beyond what 1:1 would look like on a medium density screen, as
    700         // scaling beyond that would only yield blur.
    701         mOverScaleFactor = (float) metrics.densityDpi / (float) DisplayMetrics.DENSITY_MEDIUM;
    702         if (mOverScaleFactor < 1f) {
    703             mOverScaleFactor = 1f;
    704         }
    705     }
    706 
    707     /**
    708      * Returns the controller.
    709      *
    710      * @return The {@code Controller}.
    711      */
    712     public Controller getController() {
    713         return mController;
    714     }
    715 
    716     public void setListener(Listener l) {
    717         mListener = l;
    718     }
    719 
    720     public void setViewGap(int viewGap) {
    721         mViewGap = viewGap;
    722     }
    723 
    724     /**
    725      * Sets the helper that's to be used to open photo sphere panoramas.
    726      */
    727     public void setPanoramaViewHelper(PanoramaViewHelper helper) {
    728         mPanoramaViewHelper = helper;
    729     }
    730 
    731     /**
    732      * Checks if the data is at the center.
    733      *
    734      * @param id The id of the data to check.
    735      * @return {@code True} if the data is currently at the center.
    736      */
    737     private boolean isDataAtCenter(int id) {
    738         if (mViewItem[mCurrentItem] == null) {
    739             return false;
    740         }
    741         if (mViewItem[mCurrentItem].getId() == id
    742                 && mViewItem[mCurrentItem].getCenterX() == mCenterX) {
    743             return true;
    744         }
    745         return false;
    746     }
    747 
    748     private int getCurrentViewType() {
    749         ViewItem curr = mViewItem[mCurrentItem];
    750         if (curr == null) {
    751             return ImageData.VIEW_TYPE_NONE;
    752         }
    753         return mDataAdapter.getImageData(curr.getId()).getViewType();
    754     }
    755 
    756     /** Returns [width, height] preserving image aspect ratio. */
    757     private int[] calculateChildDimension(
    758             int imageWidth, int imageHeight, int imageOrientation,
    759             int boundWidth, int boundHeight) {
    760         if (imageOrientation == 90 || imageOrientation == 270) {
    761             // Swap width and height.
    762             int savedWidth = imageWidth;
    763             imageWidth = imageHeight;
    764             imageHeight = savedWidth;
    765         }
    766         if (imageWidth == ImageData.SIZE_FULL
    767                 || imageHeight == ImageData.SIZE_FULL) {
    768             imageWidth = boundWidth;
    769             imageHeight = boundHeight;
    770         }
    771 
    772         int[] ret = new int[2];
    773         ret[0] = boundWidth;
    774         ret[1] = boundHeight;
    775 
    776         if (imageWidth * ret[1] > ret[0] * imageHeight) {
    777             ret[1] = imageHeight * ret[0] / imageWidth;
    778         } else {
    779             ret[0] = imageWidth * ret[1] / imageHeight;
    780         }
    781 
    782         return ret;
    783     }
    784 
    785     private void measureViewItem(ViewItem item, int boundWidth, int boundHeight) {
    786         int id = item.getId();
    787         ImageData imageData = mDataAdapter.getImageData(id);
    788         if (imageData == null) {
    789             Log.e(TAG, "trying to measure a null item");
    790             return;
    791         }
    792 
    793         int[] dim = calculateChildDimension(imageData.getWidth(), imageData.getHeight(),
    794                 imageData.getOrientation(), boundWidth, boundHeight);
    795 
    796         item.getView().measure(
    797                 MeasureSpec.makeMeasureSpec(
    798                         dim[0], MeasureSpec.EXACTLY),
    799                 MeasureSpec.makeMeasureSpec(
    800                         dim[1], MeasureSpec.EXACTLY));
    801     }
    802 
    803     @Override
    804     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    805         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    806 
    807         int boundWidth = MeasureSpec.getSize(widthMeasureSpec);
    808         int boundHeight = MeasureSpec.getSize(heightMeasureSpec);
    809         if (boundWidth == 0 || boundHeight == 0) {
    810             // Either width or height is unknown, can't measure children yet.
    811             return;
    812         }
    813 
    814         if (mDataAdapter != null) {
    815             mDataAdapter.suggestViewSizeBound(boundWidth / 2, boundHeight / 2);
    816         }
    817 
    818         for (ViewItem item : mViewItem) {
    819             if (item != null) {
    820                 measureViewItem(item, boundWidth, boundHeight);
    821             }
    822         }
    823         clampCenterX();
    824         // Measure zoom view
    825         mZoomView.measure(
    826                 MeasureSpec.makeMeasureSpec(
    827                         widthMeasureSpec, MeasureSpec.EXACTLY),
    828                 MeasureSpec.makeMeasureSpec(
    829                         heightMeasureSpec, MeasureSpec.EXACTLY));
    830     }
    831 
    832     @Override
    833     protected boolean fitSystemWindows(Rect insets) {
    834         // Since the camera preview needs this callback to layout the camera
    835         // controls correctly, we need to call super here.
    836         super.fitSystemWindows(insets);
    837         // After calling super, we need to return false because we have other
    838         // layouts such as bottom controls that needs this callback. The
    839         // framework behavior is to stop propagating this after the first
    840         // child returning true is found.
    841         return false;
    842     }
    843 
    844     private int findTheNearestView(int pointX) {
    845 
    846         int nearest = 0;
    847         // Find the first non-null ViewItem.
    848         while (nearest < BUFFER_SIZE
    849                 && (mViewItem[nearest] == null || mViewItem[nearest].getLeftPosition() == -1)) {
    850             nearest++;
    851         }
    852         // No existing available ViewItem
    853         if (nearest == BUFFER_SIZE) {
    854             return -1;
    855         }
    856 
    857         int min = Math.abs(pointX - mViewItem[nearest].getCenterX());
    858 
    859         for (int itemID = nearest + 1; itemID < BUFFER_SIZE && mViewItem[itemID] != null; itemID++) {
    860             // Not measured yet.
    861             if (mViewItem[itemID].getLeftPosition() == -1)
    862                 continue;
    863 
    864             int c = mViewItem[itemID].getCenterX();
    865             int dist = Math.abs(pointX - c);
    866             if (dist < min) {
    867                 min = dist;
    868                 nearest = itemID;
    869             }
    870         }
    871         return nearest;
    872     }
    873 
    874     private ViewItem buildItemFromData(int dataID) {
    875         ImageData data = mDataAdapter.getImageData(dataID);
    876         if (data == null) {
    877             return null;
    878         }
    879         data.prepare();
    880         View v = mDataAdapter.getView(mActivity, dataID);
    881         if (v == null) {
    882             return null;
    883         }
    884         ViewItem item = new ViewItem(dataID, v, mViewItemUpdateListener);
    885         v = item.getView();
    886         if (v != mCameraView) {
    887             addView(item.getView());
    888         } else {
    889             v.setVisibility(View.VISIBLE);
    890             v.setAlpha(1f);
    891             v.setTranslationX(0);
    892             v.setTranslationY(0);
    893         }
    894         return item;
    895     }
    896 
    897     private void removeItem(int itemID) {
    898         if (itemID >= mViewItem.length || mViewItem[itemID] == null) {
    899             return;
    900         }
    901         ImageData data = mDataAdapter.getImageData(mViewItem[itemID].getId());
    902         if (data == null) {
    903             Log.e(TAG, "trying to remove a null item");
    904             return;
    905         }
    906         checkForRemoval(data, mViewItem[itemID].getView());
    907         mViewItem[itemID] = null;
    908     }
    909 
    910     /**
    911      * We try to keep the one closest to the center of the screen at position
    912      * mCurrentItem.
    913      */
    914     private void stepIfNeeded() {
    915         if (!inFilmStrip() && !inFullScreen()) {
    916             // The good timing to step to the next view is when everything is
    917             // not in transition.
    918             return;
    919         }
    920         final int nearest = findTheNearestView(mCenterX);
    921         // no change made.
    922         if (nearest == -1 || nearest == mCurrentItem) {
    923             return;
    924         }
    925 
    926         // Going to change the current item, notify the listener.
    927         if (mListener != null) {
    928             mListener.onDataFocusChanged(mViewItem[mCurrentItem].getId(), false);
    929         }
    930         final int adjust = nearest - mCurrentItem;
    931         if (adjust > 0) {
    932             for (int k = 0; k < adjust; k++) {
    933                 removeItem(k);
    934             }
    935             for (int k = 0; k + adjust < BUFFER_SIZE; k++) {
    936                 mViewItem[k] = mViewItem[k + adjust];
    937             }
    938             for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) {
    939                 mViewItem[k] = null;
    940                 if (mViewItem[k - 1] != null) {
    941                     mViewItem[k] = buildItemFromData(mViewItem[k - 1].getId() + 1);
    942                 }
    943             }
    944             adjustChildZOrder();
    945         } else {
    946             for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) {
    947                 removeItem(k);
    948             }
    949             for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) {
    950                 mViewItem[k] = mViewItem[k + adjust];
    951             }
    952             for (int k = -1 - adjust; k >= 0; k--) {
    953                 mViewItem[k] = null;
    954                 if (mViewItem[k + 1] != null) {
    955                     mViewItem[k] = buildItemFromData(mViewItem[k + 1].getId() - 1);
    956                 }
    957             }
    958         }
    959         invalidate();
    960         if (mListener != null) {
    961             mListener.onDataFocusChanged(mViewItem[mCurrentItem].getId(), true);
    962         }
    963     }
    964 
    965     /**
    966      * Check the bounds of {@code mCenterX}. Always call this function after:
    967      * 1. Any changes to {@code mCenterX}. 2. Any size change of the view
    968      * items.
    969      *
    970      * @return Whether clamp happened.
    971      */
    972     private boolean clampCenterX() {
    973         ViewItem curr = mViewItem[mCurrentItem];
    974         if (curr == null) {
    975             return false;
    976         }
    977 
    978         boolean stopScroll = false;
    979         if (curr.getId() == 0 && mCenterX < curr.getCenterX()
    980                 && mDataIdOnUserScrolling <= 1) {
    981             // Stop at the first ViewItem.
    982             stopScroll = true;
    983         } else if(curr.getId() == 1 && mCenterX < curr.getCenterX()
    984                 && mDataIdOnUserScrolling > 1 && mController.isScrolling()) {
    985             stopScroll = true;
    986         } if (curr.getId() == mDataAdapter.getTotalNumber() - 1
    987                 && mCenterX > curr.getCenterX()) {
    988             // Stop at the end.
    989             stopScroll = true;
    990         }
    991 
    992         if (stopScroll) {
    993             mCenterX = curr.getCenterX();
    994         }
    995 
    996         return stopScroll;
    997     }
    998 
    999     /**
   1000      * Checks if the item is centered in the film strip, and calls
   1001      * {@link #onCurrentDataCentered} or {@link #onCurrentDataOffCentered}.
   1002      * TODO: refactor.
   1003      *
   1004      * @param dataID the ID of the image data.
   1005      */
   1006     private void checkCurrentDataCentered(int dataID) {
   1007         if (mListener != null) {
   1008             if (isDataAtCenter(dataID)) {
   1009                 mListener.onCurrentDataCentered(dataID);
   1010             } else {
   1011                 mListener.onCurrentDataOffCentered(dataID);
   1012             }
   1013         }
   1014     }
   1015 
   1016     /**
   1017      * Reorders the child views to be consistent with their data ID. This
   1018      * method should be called after adding/removing views.
   1019      */
   1020     private void adjustChildZOrder() {
   1021         for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
   1022             if (mViewItem[i] == null)
   1023                 continue;
   1024             bringChildToFront(mViewItem[i].getView());
   1025         }
   1026         // ZoomView is a special case to always be in the front.
   1027         bringChildToFront(mZoomView);
   1028     }
   1029 
   1030     /**
   1031      * If the current photo is a photo sphere, this will launch the Photo Sphere
   1032      * panorama viewer.
   1033      */
   1034     @Override
   1035     public void onViewPhotoSphere() {
   1036         ViewItem curr = mViewItem[mCurrentItem];
   1037         if (curr != null) {
   1038             mDataAdapter.getImageData(curr.getId()).viewPhotoSphere(mPanoramaViewHelper);
   1039         }
   1040     }
   1041 
   1042     @Override
   1043     public void onEdit() {
   1044         ImageData data = mDataAdapter.getImageData(getCurrentId());
   1045         if (data == null || !(data instanceof LocalData)) {
   1046             return;
   1047         }
   1048         mActivity.launchEditor((LocalData) data);
   1049     }
   1050 
   1051     @Override
   1052     public void onTinyPlanet() {
   1053         ImageData data = mDataAdapter.getImageData(getCurrentId());
   1054         if (data == null || !(data instanceof LocalData)) {
   1055             return;
   1056         }
   1057         mActivity.launchTinyPlanetEditor((LocalData) data);
   1058     }
   1059 
   1060     /**
   1061      * @return The ID of the current item, or -1.
   1062      */
   1063     public int getCurrentId() {
   1064         ViewItem current = mViewItem[mCurrentItem];
   1065         if (current == null) {
   1066             return -1;
   1067         }
   1068         return current.getId();
   1069     }
   1070 
   1071     /**
   1072      * Updates the visibility of the bottom controls.
   1073      *
   1074      * @param force update the bottom controls even if the current id
   1075      *              has been checked for button visibilities
   1076      */
   1077     private void updateBottomControls(boolean force) {
   1078         if (mActivity.isSecureCamera()) {
   1079             // We cannot show buttons in secure camera that send out of app intents,
   1080             // because another app with the same name can parade as the intented
   1081             // Activity.
   1082             return;
   1083         }
   1084 
   1085         if (mBottomControls == null) {
   1086             mBottomControls = (FilmstripBottomControls) ((View) getParent())
   1087                     .findViewById(R.id.filmstrip_bottom_controls);
   1088             mActivity.setOnActionBarVisibilityListener(mBottomControls);
   1089             mBottomControls.setListener(this);
   1090         }
   1091 
   1092         final int requestId = getCurrentId();
   1093         if (requestId < 0) {
   1094             return;
   1095         }
   1096 
   1097         // We cannot rely on the requestIds alone to check for data changes,
   1098         // because an item hands its id to its rightmost neighbor on deletion.
   1099         // To avoid loading the ImageData, we also check if the DataAdapter
   1100         // has fewer total items.
   1101         int total = mDataAdapter.getTotalNumber();
   1102         if (!force && requestId == mLastItemId && mLastTotalNumber == total) {
   1103             return;
   1104         }
   1105         mLastTotalNumber = total;
   1106 
   1107         ImageData data = mDataAdapter.getImageData(requestId);
   1108 
   1109         // We can only edit photos, not videos.
   1110         mBottomControls.setEditButtonVisibility(data.isPhoto());
   1111 
   1112         // If this is a photo sphere, show the button to view it. If it's a full
   1113         // 360 photo sphere, show the tiny planet button.
   1114         if (data.getViewType() == ImageData.VIEW_TYPE_STICKY) {
   1115             // This is a workaround to prevent an unnecessary update of
   1116             // PhotoSphere metadata which fires a data focus change callback
   1117             // at a weird timing.
   1118             return;
   1119         }
   1120         // TODO: Remove this from FilmstripView as it breaks the design.
   1121         data.isPhotoSphere(mActivity, new PanoramaSupportCallback() {
   1122             @Override
   1123             public void panoramaInfoAvailable(final boolean isPanorama,
   1124                     boolean isPanorama360) {
   1125                 // Make sure the returned data is for the current image.
   1126                 if (requestId == getCurrentId()) {
   1127                     if (mListener != null) {
   1128                         // TODO: Remove this hack since there is no data focus
   1129                         // change actually.
   1130                         mListener.onDataFocusChanged(requestId, true);
   1131                     }
   1132                     mBottomControls.setViewPhotoSphereButtonVisibility(isPanorama);
   1133                     mBottomControls.setTinyPlanetButtonVisibility(isPanorama360);
   1134                 }
   1135             }
   1136         });
   1137     }
   1138 
   1139     /**
   1140      * Keep the current item in the center. This functions does not check if
   1141      * the current item is null.
   1142      */
   1143     private void snapInCenter() {
   1144         final ViewItem currentItem = mViewItem[mCurrentItem];
   1145         final int currentViewCenter = currentItem.getCenterX();
   1146         if (mController.isScrolling() || mIsUserScrolling
   1147                 || mCenterX == currentViewCenter) {
   1148             return;
   1149         }
   1150 
   1151         int snapInTime = (int) (SNAP_IN_CENTER_TIME_MS
   1152                 * ((float) Math.abs(mCenterX - currentViewCenter))
   1153                 /  mDrawArea.width());
   1154         mController.scrollToPosition(currentViewCenter,
   1155                 snapInTime, false);
   1156         if (getCurrentViewType() == ImageData.VIEW_TYPE_STICKY
   1157                 && !mController.isScaling()
   1158                 && mScale != FULL_SCREEN_SCALE) {
   1159             // Now going to full screen camera
   1160             mController.goToFullScreen();
   1161         }
   1162     }
   1163 
   1164     /**
   1165      * Translates the {@link ViewItem} on the left of the current one to match
   1166      * the full-screen layout. In full-screen, we show only one {@link ViewItem}
   1167      * which occupies the whole screen. The other left ones are put on the left
   1168      * side in full scales. Does nothing if there's no next item.
   1169      *
   1170      * @param currItem The item ID of the current one to be translated.
   1171      * @param drawAreaWidth The width of the current draw area.
   1172      * @param scaleFraction A {@code float} between 0 and 1. 0 if the current
   1173      *                      scale is {@link FILM_STRIP_SCALE}. 1 if the
   1174      *                      current scale is {@link FULL_SCREEN_SCALE}.
   1175      */
   1176     private void translateLeftViewItem(
   1177             int currItem, int drawAreaWidth, float scaleFraction) {
   1178         if (currItem < 0 || currItem > BUFFER_SIZE - 1) {
   1179             Log.e(TAG, "currItem id out of bound.");
   1180             return;
   1181         }
   1182 
   1183         final ViewItem curr = mViewItem[currItem];
   1184         final ViewItem next = mViewItem[currItem + 1];
   1185         if (curr == null || next == null) {
   1186             Log.e(TAG, "Invalid view item (curr or next == null). curr = "
   1187                     + currItem);
   1188             return;
   1189         }
   1190 
   1191         final int currCenterX = curr.getCenterX();
   1192         final int nextCenterX = next.getCenterX();
   1193         final int translate = (int) ((nextCenterX - drawAreaWidth
   1194                     - currCenterX) * scaleFraction);
   1195 
   1196         curr.layoutIn(mDrawArea, mCenterX, mScale);
   1197         curr.getView().setAlpha(1f);
   1198 
   1199         if (inFullScreen()) {
   1200             curr.setTranslationX(translate * (mCenterX - currCenterX)
   1201                     / (nextCenterX - currCenterX), mScale);
   1202         } else {
   1203             curr.setTranslationX(translate, mScale);
   1204         }
   1205     }
   1206 
   1207     /**
   1208      * Fade out the {@link ViewItem} on the right of the current one in
   1209      * full-screen layout. Does nothing if there's no previous item.
   1210      *
   1211      * @param currItem The ID of the item to fade.
   1212      */
   1213     private void fadeAndScaleRightViewItem(int currItem) {
   1214         if (currItem < 1 || currItem > BUFFER_SIZE) {
   1215             Log.e(TAG, "currItem id out of bound.");
   1216             return;
   1217         }
   1218 
   1219         final ViewItem curr = mViewItem[currItem];
   1220         final ViewItem prev = mViewItem[currItem - 1];
   1221         if (curr == null || prev == null) {
   1222             Log.e(TAG, "Invalid view item (curr or prev == null). curr = "
   1223                     + currItem);
   1224             return;
   1225         }
   1226 
   1227         final View currView = curr.getView();
   1228         if (currItem > mCurrentItem + 1) {
   1229             // Every item not right next to the mCurrentItem is invisible.
   1230             currView.setVisibility(INVISIBLE);
   1231             return;
   1232         }
   1233         final int prevCenterX = prev.getCenterX();
   1234         if (mCenterX <= prevCenterX) {
   1235             // Shortcut. If the position is at the center of the previous one,
   1236             // set to invisible too.
   1237             currView.setVisibility(INVISIBLE);
   1238             return;
   1239         }
   1240         final int currCenterX = curr.getCenterX();
   1241         final float fadeDownFraction =
   1242                 ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
   1243         curr.layoutIn(mDrawArea, currCenterX,
   1244                 FILM_STRIP_SCALE + (1f - FILM_STRIP_SCALE) * fadeDownFraction);
   1245         currView.setAlpha(fadeDownFraction);
   1246         currView.setTranslationX(0);
   1247         currView.setVisibility(VISIBLE);
   1248     }
   1249 
   1250     private void layoutViewItems(boolean layoutChanged) {
   1251         if (mViewItem[mCurrentItem] == null ||
   1252                 mDrawArea.width() == 0 ||
   1253                 mDrawArea.height() == 0) {
   1254             return;
   1255         }
   1256 
   1257         // If the layout changed, we need to adjust the current position so
   1258         // that if an item is centered before the change, it's still centered.
   1259         if (layoutChanged) {
   1260             mViewItem[mCurrentItem].setLeftPosition(
   1261                     mCenterX - mViewItem[mCurrentItem].getView().getMeasuredWidth() / 2);
   1262         }
   1263 
   1264         if (mController.isZoomStarted()) {
   1265             return;
   1266         }
   1267         /**
   1268          * Transformed scale fraction between 0 and 1. 0 if the scale is
   1269          * {@link FILM_STRIP_SCALE}. 1 if the scale is {@link FULL_SCREEN_SCALE}
   1270          * .
   1271          */
   1272         final float scaleFraction = mViewAnimInterpolator.getInterpolation(
   1273                 (mScale - FILM_STRIP_SCALE) / (FULL_SCREEN_SCALE - FILM_STRIP_SCALE));
   1274         final int fullScreenWidth = mDrawArea.width() + mViewGap;
   1275 
   1276         // Decide the position for all view items on the left and the right first.
   1277 
   1278         // Left items.
   1279         for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
   1280             final ViewItem curr = mViewItem[itemID];
   1281             if (curr == null) {
   1282                 break;
   1283             }
   1284 
   1285             // First, layout relatively to the next one.
   1286             final int currLeft = mViewItem[itemID + 1].getLeftPosition()
   1287                     - curr.getView().getMeasuredWidth() - mViewGap;
   1288             curr.setLeftPosition(currLeft);
   1289         }
   1290         // Right items.
   1291         for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
   1292             final ViewItem curr = mViewItem[itemID];
   1293             if (curr == null) {
   1294                 break;
   1295             }
   1296 
   1297             // First, layout relatively to the previous one.
   1298             final ViewItem prev = mViewItem[itemID - 1];
   1299             final int currLeft =
   1300                     prev.getLeftPosition() + prev.getView().getMeasuredWidth()
   1301                             + mViewGap;
   1302             curr.setLeftPosition(currLeft);
   1303         }
   1304 
   1305         // Special case for the one immediately on the right of the camera
   1306         // preview.
   1307         boolean immediateRight =
   1308                 (mViewItem[mCurrentItem].getId() == 1 &&
   1309                 mDataAdapter.getImageData(0).getViewType() == ImageData.VIEW_TYPE_STICKY);
   1310 
   1311         // Layout the current ViewItem first.
   1312         if (immediateRight) {
   1313             // Just do a simple layout without any special translation or
   1314             // fading.  The implementation in Gallery does not push the first
   1315             // photo to the bottom of the camera preview. Simply place the
   1316             // photo on the right of the preview.
   1317             final ViewItem currItem = mViewItem[mCurrentItem];
   1318             currItem.layoutIn(mDrawArea, mCenterX, mScale);
   1319             currItem.setTranslationX(0f, mScale);
   1320             currItem.getView().setAlpha(1f);
   1321         } else if (scaleFraction == 1f) {
   1322             final ViewItem currItem = mViewItem[mCurrentItem];
   1323             final int currCenterX = currItem.getCenterX();
   1324             if (mCenterX < currCenterX) {
   1325                 // In full-screen and mCenterX is on the left of the center,
   1326                 // we draw the current one to "fade down".
   1327                 fadeAndScaleRightViewItem(mCurrentItem);
   1328             } else if(mCenterX > currCenterX) {
   1329                 // In full-screen and mCenterX is on the right of the center,
   1330                 // we draw the current one translated.
   1331                 translateLeftViewItem(mCurrentItem, fullScreenWidth, scaleFraction);
   1332             } else {
   1333                 currItem.layoutIn(mDrawArea, mCenterX, mScale);
   1334                 currItem.setTranslationX(0f, mScale);
   1335                 currItem.getView().setAlpha(1f);
   1336             }
   1337         } else {
   1338             final ViewItem currItem = mViewItem[mCurrentItem];
   1339             // The normal filmstrip has no translation for the current item. If it has
   1340             // translation before, gradually set it to zero.
   1341             currItem.setTranslationX(
   1342                     currItem.getScaledTranslationX(mScale) * scaleFraction,
   1343                     mScale);
   1344             currItem.layoutIn(mDrawArea, mCenterX, mScale);
   1345             if (mViewItem[mCurrentItem - 1] == null) {
   1346                 currItem.getView().setAlpha(1f);
   1347             } else {
   1348                 final int currCenterX = currItem.getCenterX();
   1349                 final int prevCenterX = mViewItem[mCurrentItem - 1].getCenterX();
   1350                 final float fadeDownFraction =
   1351                         ((float) mCenterX - prevCenterX) / (currCenterX - prevCenterX);
   1352                 currItem.getView().setAlpha(
   1353                         (1 - fadeDownFraction) * (1 - scaleFraction) + fadeDownFraction);
   1354             }
   1355         }
   1356 
   1357         // Layout the rest dependent on the current scale.
   1358 
   1359         // Items on the left
   1360         for (int itemID = mCurrentItem - 1; itemID >= 0; itemID--) {
   1361             final ViewItem curr = mViewItem[itemID];
   1362             if (curr == null) {
   1363                 break;
   1364             }
   1365             translateLeftViewItem(itemID, fullScreenWidth, scaleFraction);
   1366         }
   1367 
   1368         // Items on the right
   1369         for (int itemID = mCurrentItem + 1; itemID < BUFFER_SIZE; itemID++) {
   1370             final ViewItem curr = mViewItem[itemID];
   1371             if (curr == null) {
   1372                 break;
   1373             }
   1374 
   1375             curr.layoutIn(mDrawArea, mCenterX, mScale);
   1376             if (curr.getId() == 1 && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   1377                 // Special case for the one next to the camera preview.
   1378                 curr.getView().setAlpha(1f);
   1379                 continue;
   1380             }
   1381 
   1382             final View currView = curr.getView();
   1383             if (scaleFraction == 1) {
   1384                 // It's in full-screen mode.
   1385                 fadeAndScaleRightViewItem(itemID);
   1386             } else {
   1387                 if (currView.getVisibility() == INVISIBLE) {
   1388                     currView.setVisibility(VISIBLE);
   1389                 }
   1390                 if (itemID == mCurrentItem + 1) {
   1391                     currView.setAlpha(1f - scaleFraction);
   1392                 } else {
   1393                     if (scaleFraction == 0f) {
   1394                         currView.setAlpha(1f);
   1395                     } else {
   1396                         currView.setVisibility(INVISIBLE);
   1397                     }
   1398                 }
   1399                 curr.setTranslationX(
   1400                         (mViewItem[mCurrentItem].getLeftPosition() - curr.getLeftPosition())
   1401                         * scaleFraction, mScale);
   1402             }
   1403         }
   1404 
   1405         stepIfNeeded();
   1406         updateBottomControls(false /* no forced update */);
   1407         mLastItemId = getCurrentId();
   1408     }
   1409 
   1410     @Override
   1411     public void onDraw(Canvas c) {
   1412         // TODO: remove layoutViewItems() here.
   1413         layoutViewItems(false);
   1414         super.onDraw(c);
   1415     }
   1416 
   1417     @Override
   1418     protected void onLayout(boolean changed, int l, int t, int r, int b) {
   1419         mDrawArea.left = l;
   1420         mDrawArea.top = t;
   1421         mDrawArea.right = r;
   1422         mDrawArea.bottom = b;
   1423         mZoomView.layout(mDrawArea.left, mDrawArea.top, mDrawArea.right, mDrawArea.bottom);
   1424         // TODO: Need a more robust solution to decide when to re-layout
   1425         // If in the middle of zooming, only re-layout when the layout has changed.
   1426         if (!mController.isZoomStarted() || changed) {
   1427             resetZoomView();
   1428             layoutViewItems(changed);
   1429         }
   1430     }
   1431 
   1432     /**
   1433      * Clears the translation and scale that has been set on the view, cancels any loading
   1434      * request for image partial decoding, and hides zoom view.
   1435      * This is needed for when there is a layout change (e.g. when users re-enter the app,
   1436      * or rotate the device, etc).
   1437      */
   1438     private void resetZoomView() {
   1439         if (!mController.isZoomStarted()) {
   1440             return;
   1441         }
   1442         ViewItem current = mViewItem[mCurrentItem];
   1443         if (current == null) {
   1444             return;
   1445         }
   1446         mScale = FULL_SCREEN_SCALE;
   1447         mController.cancelZoomAnimation();
   1448         mController.cancelFlingAnimation();
   1449         current.resetTransform();
   1450         mController.cancelLoadingZoomedImage();
   1451         mZoomView.setVisibility(GONE);
   1452         mController.setSurroundingViewsVisible(true);
   1453     }
   1454 
   1455     private void hideZoomView() {
   1456         if (mController.isZoomStarted()) {
   1457             mController.cancelLoadingZoomedImage();
   1458             mZoomView.setVisibility(GONE);
   1459         }
   1460     }
   1461 
   1462     // Keeps the view in the view hierarchy if it's camera preview.
   1463     // Remove from the hierarchy otherwise.
   1464     private void checkForRemoval(ImageData data, View v) {
   1465         if (data.getViewType() != ImageData.VIEW_TYPE_STICKY) {
   1466             removeView(v);
   1467             data.recycle();
   1468         } else {
   1469             v.setVisibility(View.INVISIBLE);
   1470             if (mCameraView != null && mCameraView != v) {
   1471                 removeView(mCameraView);
   1472             }
   1473             mCameraView = v;
   1474         }
   1475     }
   1476 
   1477     private void slideViewBack(ViewItem item) {
   1478         item.animateTranslationX(
   1479                 0, GEOMETRY_ADJUST_TIME_MS, mViewAnimInterpolator);
   1480         item.getView().animate()
   1481                 .alpha(1f)
   1482                 .setDuration(GEOMETRY_ADJUST_TIME_MS)
   1483                 .setInterpolator(mViewAnimInterpolator)
   1484                 .start();
   1485     }
   1486 
   1487     private void animateItemRemoval(int dataID, final ImageData data) {
   1488         int removedItem = findItemByDataID(dataID);
   1489 
   1490         // adjust the data id to be consistent
   1491         for (int i = 0; i < BUFFER_SIZE; i++) {
   1492             if (mViewItem[i] == null || mViewItem[i].getId() <= dataID) {
   1493                 continue;
   1494             }
   1495             mViewItem[i].setId(mViewItem[i].getId() - 1);
   1496         }
   1497         if (removedItem == -1) {
   1498             return;
   1499         }
   1500 
   1501         final View removedView = mViewItem[removedItem].getView();
   1502         final int offsetX = removedView.getMeasuredWidth() + mViewGap;
   1503 
   1504         for (int i = removedItem + 1; i < BUFFER_SIZE; i++) {
   1505             if (mViewItem[i] != null) {
   1506                 mViewItem[i].setLeftPosition(mViewItem[i].getLeftPosition() - offsetX);
   1507             }
   1508         }
   1509 
   1510         if (removedItem >= mCurrentItem
   1511                 && mViewItem[removedItem].getId() < mDataAdapter.getTotalNumber()) {
   1512             // Fill the removed item by left shift when the current one or
   1513             // anyone on the right is removed, and there's more data on the
   1514             // right available.
   1515             for (int i = removedItem; i < BUFFER_SIZE - 1; i++) {
   1516                 mViewItem[i] = mViewItem[i + 1];
   1517             }
   1518 
   1519             // pull data out from the DataAdapter for the last one.
   1520             int curr = BUFFER_SIZE - 1;
   1521             int prev = curr - 1;
   1522             if (mViewItem[prev] != null) {
   1523                 mViewItem[curr] = buildItemFromData(mViewItem[prev].getId() + 1);
   1524             }
   1525 
   1526             // The animation part.
   1527             if (inFullScreen()) {
   1528                 mViewItem[mCurrentItem].getView().setVisibility(VISIBLE);
   1529                 ViewItem nextItem = mViewItem[mCurrentItem + 1];
   1530                 if (nextItem != null) {
   1531                     nextItem.getView().setVisibility(INVISIBLE);
   1532                 }
   1533             }
   1534 
   1535             // Translate the views to their original places.
   1536             for (int i = removedItem; i < BUFFER_SIZE; i++) {
   1537                 if (mViewItem[i] != null) {
   1538                     mViewItem[i].setTranslationX(offsetX, mScale);
   1539                 }
   1540             }
   1541 
   1542             // The end of the filmstrip might have been changed.
   1543             // The mCenterX might be out of the bound.
   1544             ViewItem currItem = mViewItem[mCurrentItem];
   1545             if (currItem.getId() == mDataAdapter.getTotalNumber() - 1
   1546                     && mCenterX > currItem.getCenterX()) {
   1547                 int adjustDiff = currItem.getCenterX() - mCenterX;
   1548                 mCenterX = currItem.getCenterX();
   1549                 for (int i = 0; i < BUFFER_SIZE; i++) {
   1550                     if (mViewItem[i] != null) {
   1551                         mViewItem[i].translateXBy(adjustDiff, mScale);
   1552                     }
   1553                 }
   1554             }
   1555         } else {
   1556             // fill the removed place by right shift
   1557             mCenterX -= offsetX;
   1558 
   1559             for (int i = removedItem; i > 0; i--) {
   1560                 mViewItem[i] = mViewItem[i - 1];
   1561             }
   1562 
   1563             // pull data out from the DataAdapter for the first one.
   1564             int curr = 0;
   1565             int next = curr + 1;
   1566             if (mViewItem[next] != null) {
   1567                 mViewItem[curr] = buildItemFromData(mViewItem[next].getId() - 1);
   1568             }
   1569 
   1570             // Translate the views to their original places.
   1571             for (int i = removedItem; i >= 0; i--) {
   1572                 if (mViewItem[i] != null) {
   1573                     mViewItem[i].setTranslationX(-offsetX, mScale);
   1574                 }
   1575             }
   1576         }
   1577 
   1578         // Now, slide every one back.
   1579         for (int i = 0; i < BUFFER_SIZE; i++) {
   1580             if (mViewItem[i] != null
   1581                     && mViewItem[i].getScaledTranslationX(mScale) != 0f) {
   1582                 slideViewBack(mViewItem[i]);
   1583             }
   1584         }
   1585         if (mCenterX == mViewItem[mCurrentItem].getCenterX()
   1586                 && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   1587             // Special case for scrolling onto the camera preview after removal.
   1588             mController.goToFullScreen();
   1589         }
   1590 
   1591         int transY = getHeight() / 8;
   1592         if (removedView.getTranslationY() < 0) {
   1593             transY = -transY;
   1594         }
   1595         removedView.animate()
   1596                 .alpha(0f)
   1597                 .translationYBy(transY)
   1598                 .setInterpolator(mViewAnimInterpolator)
   1599                 .setDuration(GEOMETRY_ADJUST_TIME_MS)
   1600                 .setListener(new Animator.AnimatorListener() {
   1601                     @Override
   1602                     public void onAnimationStart(Animator animation) {
   1603                         // Do nothing.
   1604                     }
   1605 
   1606                     @Override
   1607                     public void onAnimationEnd(Animator animation) {
   1608                         checkForRemoval(data, removedView);
   1609                     }
   1610 
   1611                     @Override
   1612                     public void onAnimationCancel(Animator animation) {
   1613                         // Do nothing.
   1614                     }
   1615 
   1616                     @Override
   1617                     public void onAnimationRepeat(Animator animation) {
   1618                         // Do nothing.
   1619                     }
   1620                 })
   1621                 .start();
   1622         adjustChildZOrder();
   1623         invalidate();
   1624     }
   1625 
   1626     // returns -1 on failure.
   1627     private int findItemByDataID(int dataID) {
   1628         for (int i = 0; i < BUFFER_SIZE; i++) {
   1629             if (mViewItem[i] != null
   1630                     && mViewItem[i].getId() == dataID) {
   1631                 return i;
   1632             }
   1633         }
   1634         return -1;
   1635     }
   1636 
   1637     private void updateInsertion(int dataID) {
   1638         int insertedItem = findItemByDataID(dataID);
   1639         if (insertedItem == -1) {
   1640             // Not in the current item buffers. Check if it's inserted
   1641             // at the end.
   1642             if (dataID == mDataAdapter.getTotalNumber() - 1) {
   1643                 int prev = findItemByDataID(dataID - 1);
   1644                 if (prev >= 0 && prev < BUFFER_SIZE - 1) {
   1645                     // The previous data is in the buffer and we still
   1646                     // have room for the inserted data.
   1647                     insertedItem = prev + 1;
   1648                 }
   1649             }
   1650         }
   1651 
   1652         // adjust the data id to be consistent
   1653         for (int i = 0; i < BUFFER_SIZE; i++) {
   1654             if (mViewItem[i] == null || mViewItem[i].getId() < dataID) {
   1655                 continue;
   1656             }
   1657             mViewItem[i].setId(mViewItem[i].getId() + 1);
   1658         }
   1659         if (insertedItem == -1) {
   1660             return;
   1661         }
   1662 
   1663         final ImageData data = mDataAdapter.getImageData(dataID);
   1664         int[] dim = calculateChildDimension(
   1665                 data.getWidth(), data.getHeight(), data.getOrientation(),
   1666                 getMeasuredWidth(), getMeasuredHeight());
   1667         final int offsetX = dim[0] + mViewGap;
   1668         ViewItem viewItem = buildItemFromData(dataID);
   1669 
   1670         if (insertedItem >= mCurrentItem) {
   1671             if (insertedItem == mCurrentItem) {
   1672                 viewItem.setLeftPosition(mViewItem[mCurrentItem].getLeftPosition());
   1673             }
   1674             // Shift right to make rooms for newly inserted item.
   1675             removeItem(BUFFER_SIZE - 1);
   1676             for (int i = BUFFER_SIZE - 1; i > insertedItem; i--) {
   1677                 mViewItem[i] = mViewItem[i - 1];
   1678                 if (mViewItem[i] != null) {
   1679                     mViewItem[i].setTranslationX(-offsetX, mScale);
   1680                     slideViewBack(mViewItem[i]);
   1681                 }
   1682             }
   1683         } else {
   1684             // Shift left. Put the inserted data on the left instead of the
   1685             // found position.
   1686             --insertedItem;
   1687             if (insertedItem < 0) {
   1688                 return;
   1689             }
   1690             removeItem(0);
   1691             for (int i = 1; i <= insertedItem; i++) {
   1692                 if (mViewItem[i] != null) {
   1693                     mViewItem[i].setTranslationX(offsetX, mScale);
   1694                     slideViewBack(mViewItem[i]);
   1695                     mViewItem[i - 1] = mViewItem[i];
   1696                 }
   1697             }
   1698         }
   1699 
   1700         mViewItem[insertedItem] = viewItem;
   1701         View insertedView = mViewItem[insertedItem].getView();
   1702         insertedView.setAlpha(0f);
   1703         insertedView.setTranslationY(getHeight() / 8);
   1704         insertedView.animate()
   1705                 .alpha(1f)
   1706                 .translationY(0f)
   1707                 .setInterpolator(mViewAnimInterpolator)
   1708                 .setDuration(GEOMETRY_ADJUST_TIME_MS)
   1709                 .start();
   1710         adjustChildZOrder();
   1711         invalidate();
   1712     }
   1713 
   1714     public void setDataAdapter(DataAdapter adapter) {
   1715         mDataAdapter = adapter;
   1716         mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight());
   1717         mDataAdapter.setListener(new DataAdapter.Listener() {
   1718             @Override
   1719             public void onDataLoaded() {
   1720                 reload();
   1721             }
   1722 
   1723             @Override
   1724             public void onDataUpdated(DataAdapter.UpdateReporter reporter) {
   1725                 update(reporter);
   1726             }
   1727 
   1728             @Override
   1729             public void onDataInserted(int dataID, ImageData data) {
   1730                 if (mViewItem[mCurrentItem] == null) {
   1731                     // empty now, simply do a reload.
   1732                     reload();
   1733                     return;
   1734                 }
   1735                 updateInsertion(dataID);
   1736             }
   1737 
   1738             @Override
   1739             public void onDataRemoved(int dataID, ImageData data) {
   1740                 animateItemRemoval(dataID, data);
   1741             }
   1742         });
   1743     }
   1744 
   1745     public boolean inFilmStrip() {
   1746         return (mScale == FILM_STRIP_SCALE);
   1747     }
   1748 
   1749     public boolean inFullScreen() {
   1750         return (mScale == FULL_SCREEN_SCALE);
   1751     }
   1752 
   1753     public boolean isCameraPreview() {
   1754         return (getCurrentViewType() == ImageData.VIEW_TYPE_STICKY);
   1755     }
   1756 
   1757     public boolean inCameraFullscreen() {
   1758         return isDataAtCenter(0) && inFullScreen()
   1759                 && (getCurrentViewType() == ImageData.VIEW_TYPE_STICKY);
   1760     }
   1761 
   1762     @Override
   1763     public boolean onInterceptTouchEvent(MotionEvent ev) {
   1764         if (!inFullScreen() || mController.isScrolling()) {
   1765             return true;
   1766         }
   1767 
   1768         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
   1769             mCheckToIntercept = true;
   1770             mDown = MotionEvent.obtain(ev);
   1771             ViewItem viewItem = mViewItem[mCurrentItem];
   1772             // Do not intercept touch if swipe is not enabled
   1773             if (viewItem != null && !mDataAdapter.canSwipeInFullScreen(viewItem.getId())) {
   1774                 mCheckToIntercept = false;
   1775             }
   1776             return false;
   1777         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
   1778             // Do not intercept touch once child is in zoom mode
   1779             mCheckToIntercept = false;
   1780             return false;
   1781         } else {
   1782             if (!mCheckToIntercept) {
   1783                 return false;
   1784             }
   1785             if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) {
   1786                 return false;
   1787             }
   1788             int deltaX = (int) (ev.getX() - mDown.getX());
   1789             int deltaY = (int) (ev.getY() - mDown.getY());
   1790             if (ev.getActionMasked() == MotionEvent.ACTION_MOVE
   1791                     && deltaX < mSlop * (-1)) {
   1792                 // intercept left swipe
   1793                 if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) {
   1794                     UsageStatistics.onEvent(UsageStatistics.COMPONENT_CAMERA,
   1795                             UsageStatistics.ACTION_FILMSTRIP, null);
   1796                     return true;
   1797                 }
   1798             }
   1799         }
   1800         return false;
   1801     }
   1802 
   1803     @Override
   1804     public boolean onTouchEvent(MotionEvent ev) {
   1805         mGestureRecognizer.onTouchEvent(ev);
   1806         return true;
   1807     }
   1808 
   1809     private void updateViewItem(int itemID) {
   1810         ViewItem item = mViewItem[itemID];
   1811         if (item == null) {
   1812             Log.e(TAG, "trying to update an null item");
   1813             return;
   1814         }
   1815         removeView(item.getView());
   1816 
   1817         ImageData data = mDataAdapter.getImageData(item.getId());
   1818         if (data == null) {
   1819             Log.e(TAG, "trying recycle a null item");
   1820             return;
   1821         }
   1822         data.recycle();
   1823 
   1824         ViewItem newItem = buildItemFromData(item.getId());
   1825         if (newItem == null) {
   1826             Log.e(TAG, "new item is null");
   1827             // keep using the old data.
   1828             data.prepare();
   1829             addView(item.getView());
   1830             return;
   1831         }
   1832         newItem.copyGeometry(item);
   1833         mViewItem[itemID] = newItem;
   1834 
   1835         boolean stopScroll = clampCenterX();
   1836         checkCurrentDataCentered(getCurrentId());
   1837         if (stopScroll) {
   1838             mController.stopScrolling(true);
   1839         }
   1840         adjustChildZOrder();
   1841         invalidate();
   1842     }
   1843 
   1844     /** Some of the data is changed. */
   1845     private void update(DataAdapter.UpdateReporter reporter) {
   1846         // No data yet.
   1847         if (mViewItem[mCurrentItem] == null) {
   1848             reload();
   1849             return;
   1850         }
   1851 
   1852         // Check the current one.
   1853         ViewItem curr = mViewItem[mCurrentItem];
   1854         int dataId = curr.getId();
   1855         if (reporter.isDataRemoved(dataId)) {
   1856             reload();
   1857             return;
   1858         }
   1859         if (reporter.isDataUpdated(dataId)) {
   1860             updateViewItem(mCurrentItem);
   1861             final ImageData data = mDataAdapter.getImageData(dataId);
   1862             if (!mIsUserScrolling && !mController.isScrolling()) {
   1863                 // If there is no scrolling at all, adjust mCenterX to place
   1864                 // the current item at the center.
   1865                 int[] dim = calculateChildDimension(
   1866                         data.getWidth(), data.getHeight(), data.getOrientation(),
   1867                         getMeasuredWidth(), getMeasuredHeight());
   1868                 mCenterX = curr.getLeftPosition() + dim[0] / 2;
   1869             }
   1870         }
   1871 
   1872         // Check left
   1873         for (int i = mCurrentItem - 1; i >= 0; i--) {
   1874             curr = mViewItem[i];
   1875             if (curr != null) {
   1876                 dataId = curr.getId();
   1877                 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
   1878                     updateViewItem(i);
   1879                 }
   1880             } else {
   1881                 ViewItem next = mViewItem[i + 1];
   1882                 if (next != null) {
   1883                     mViewItem[i] = buildItemFromData(next.getId() - 1);
   1884                 }
   1885             }
   1886         }
   1887 
   1888         // Check right
   1889         for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
   1890             curr = mViewItem[i];
   1891             if (curr != null) {
   1892                 dataId = curr.getId();
   1893                 if (reporter.isDataRemoved(dataId) || reporter.isDataUpdated(dataId)) {
   1894                     updateViewItem(i);
   1895                 }
   1896             } else {
   1897                 ViewItem prev = mViewItem[i - 1];
   1898                 if (prev != null) {
   1899                     mViewItem[i] = buildItemFromData(prev.getId() + 1);
   1900                 }
   1901             }
   1902         }
   1903         adjustChildZOrder();
   1904         // Request a layout to find the measured width/height of the view first.
   1905         requestLayout();
   1906         // Update photo sphere visibility after metadata fully written.
   1907         updateBottomControls(true /* forced update */);
   1908     }
   1909 
   1910     /**
   1911      * The whole data might be totally different. Flush all and load from the
   1912      * start. Filmstrip will be centered on the first item, i.e. the camera
   1913      * preview.
   1914      */
   1915     private void reload() {
   1916         mController.stopScrolling(true);
   1917         mController.stopScale();
   1918         mDataIdOnUserScrolling = 0;
   1919         // Reload has a side effect that after this call, it will show the
   1920         // camera preview. So we want to know whether it starts from the camera
   1921         // preview to decide whether we need to call onDataFocusChanged.
   1922         boolean stayInPreview = false;
   1923 
   1924         if (mListener != null && mViewItem[mCurrentItem] != null) {
   1925             stayInPreview = mViewItem[mCurrentItem].getId() == 0;
   1926             if (!stayInPreview) {
   1927                 mListener.onDataFocusChanged(mViewItem[mCurrentItem].getId(), false);
   1928             }
   1929         }
   1930 
   1931         // Remove all views from the mViewItem buffer, except the camera view.
   1932         for (int i = 0; i < mViewItem.length; i++) {
   1933             if (mViewItem[i] == null) {
   1934                 continue;
   1935             }
   1936             View v = mViewItem[i].getView();
   1937             if (v != mCameraView) {
   1938                 removeView(v);
   1939             }
   1940             ImageData imageData = mDataAdapter.getImageData(mViewItem[i].getId());
   1941             if (imageData != null) {
   1942                 imageData.recycle();
   1943             }
   1944         }
   1945 
   1946         // Clear out the mViewItems and rebuild with camera in the center.
   1947         Arrays.fill(mViewItem, null);
   1948         int dataNumber = mDataAdapter.getTotalNumber();
   1949         if (dataNumber == 0) {
   1950             return;
   1951         }
   1952 
   1953         mViewItem[mCurrentItem] = buildItemFromData(0);
   1954         if (mViewItem[mCurrentItem] == null) {
   1955             return;
   1956         }
   1957         mViewItem[mCurrentItem].setLeftPosition(0);
   1958         for (int i = mCurrentItem + 1; i < BUFFER_SIZE; i++) {
   1959             mViewItem[i] = buildItemFromData(mViewItem[i - 1].getId() + 1);
   1960             if (mViewItem[i] == null) {
   1961                 break;
   1962             }
   1963         }
   1964 
   1965         // Ensure that the views in mViewItem will layout the first in the
   1966         // center of the display upon a reload.
   1967         mCenterX = -1;
   1968         mScale = FULL_SCREEN_SCALE;
   1969 
   1970         adjustChildZOrder();
   1971         invalidate();
   1972 
   1973         if (mListener != null) {
   1974             mListener.onReload();
   1975             if (!stayInPreview) {
   1976                 mListener.onDataFocusChanged(mViewItem[mCurrentItem].getId(), true);
   1977             }
   1978         }
   1979     }
   1980 
   1981     private void promoteData(int itemID, int dataID) {
   1982         if (mListener != null) {
   1983             mListener.onDataPromoted(dataID);
   1984         }
   1985     }
   1986 
   1987     private void demoteData(int itemID, int dataID) {
   1988         if (mListener != null) {
   1989             mListener.onDataDemoted(dataID);
   1990         }
   1991     }
   1992 
   1993     /**
   1994      * MyController controls all the geometry animations. It passively tells the
   1995      * geometry information on demand.
   1996      */
   1997     private class MyController implements Controller {
   1998 
   1999         private final ValueAnimator mScaleAnimator;
   2000         private ValueAnimator mZoomAnimator;
   2001         private AnimatorSet mFlingAnimator;
   2002 
   2003         private final MyScroller mScroller;
   2004         private boolean mCanStopScroll;
   2005 
   2006         private final MyScroller.Listener mScrollerListener =
   2007                 new MyScroller.Listener() {
   2008                     @Override
   2009                     public void onScrollUpdate(int currX, int currY) {
   2010                         mCenterX = currX;
   2011 
   2012                         boolean stopScroll = clampCenterX();
   2013                         checkCurrentDataCentered(getCurrentId());
   2014                         if (stopScroll) {
   2015                             mController.stopScrolling(true);
   2016                         }
   2017                         invalidate();
   2018                     }
   2019 
   2020                     @Override
   2021                     public void onScrollEnd() {
   2022                         mCanStopScroll = true;
   2023                         if (mViewItem[mCurrentItem] == null) {
   2024                             return;
   2025                         }
   2026                         snapInCenter();
   2027                         if (mCenterX == mViewItem[mCurrentItem].getCenterX()
   2028                                 && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   2029                             // Special case for the scrolling end on the camera preview.
   2030                             goToFullScreen();
   2031                         }
   2032                     }
   2033                 };
   2034 
   2035         private ValueAnimator.AnimatorUpdateListener mScaleAnimatorUpdateListener =
   2036                 new ValueAnimator.AnimatorUpdateListener() {
   2037                     @Override
   2038                     public void onAnimationUpdate(ValueAnimator animation) {
   2039                         if (mViewItem[mCurrentItem] == null) {
   2040                             return;
   2041                         }
   2042                         mScale = (Float) animation.getAnimatedValue();
   2043                         invalidate();
   2044                     }
   2045                 };
   2046 
   2047         MyController(Context context) {
   2048             TimeInterpolator decelerateInterpolator = new DecelerateInterpolator(1.5f);
   2049             mScroller = new MyScroller(mActivity,
   2050                     new Handler(mActivity.getMainLooper()),
   2051                     mScrollerListener, decelerateInterpolator);
   2052             mCanStopScroll = true;
   2053 
   2054             mScaleAnimator = new ValueAnimator();
   2055             mScaleAnimator.addUpdateListener(mScaleAnimatorUpdateListener);
   2056             mScaleAnimator.setInterpolator(decelerateInterpolator);
   2057         }
   2058 
   2059         @Override
   2060         public boolean isScrolling() {
   2061             return !mScroller.isFinished();
   2062         }
   2063 
   2064         @Override
   2065         public boolean isScaling() {
   2066             return mScaleAnimator.isRunning();
   2067         }
   2068 
   2069         private int estimateMinX(int dataID, int leftPos, int viewWidth) {
   2070             return leftPos - (dataID + 100) * (viewWidth + mViewGap);
   2071         }
   2072 
   2073         private int estimateMaxX(int dataID, int leftPos, int viewWidth) {
   2074             return leftPos
   2075                     + (mDataAdapter.getTotalNumber() - dataID + 100)
   2076                     * (viewWidth + mViewGap);
   2077         }
   2078 
   2079         /** Zoom all the way in or out on the image at the given pivot point. */
   2080         private void zoomAt(final ViewItem current, final float focusX, final float focusY) {
   2081             // End previous zoom animation, if any
   2082             if (mZoomAnimator != null) {
   2083                 mZoomAnimator.end();
   2084             }
   2085             // Calculate end scale
   2086             final float maxScale = getCurrentDataMaxScale(false);
   2087             final float endScale = mScale < maxScale - maxScale * TOLERANCE
   2088                     ? maxScale : FULL_SCREEN_SCALE;
   2089 
   2090             mZoomAnimator = new ValueAnimator();
   2091             mZoomAnimator.setFloatValues(mScale, endScale);
   2092             mZoomAnimator.setDuration(ZOOM_ANIMATION_DURATION_MS);
   2093             mZoomAnimator.addListener(new Animator.AnimatorListener() {
   2094                 @Override
   2095                 public void onAnimationStart(Animator animation) {
   2096                     if (mScale == FULL_SCREEN_SCALE) {
   2097                         enterFullScreen();
   2098                         setSurroundingViewsVisible(false);
   2099                     }
   2100                     cancelLoadingZoomedImage();
   2101                 }
   2102 
   2103                 @Override
   2104                 public void onAnimationEnd(Animator animation) {
   2105                     // Make sure animation ends up having the correct scale even
   2106                     // if it is cancelled before it finishes
   2107                     if (mScale != endScale) {
   2108                         current.postScale(focusX, focusY, endScale/mScale, mDrawArea.width(),
   2109                                 mDrawArea.height());
   2110                         mScale = endScale;
   2111                     }
   2112 
   2113                     if (mScale == FULL_SCREEN_SCALE) {
   2114                         setSurroundingViewsVisible(true);
   2115                         mZoomView.setVisibility(GONE);
   2116                         current.resetTransform();
   2117                     } else {
   2118                         mController.loadZoomedImage();
   2119                     }
   2120                     mZoomAnimator = null;
   2121                 }
   2122 
   2123                 @Override
   2124                 public void onAnimationCancel(Animator animation) {
   2125                     // Do nothing.
   2126                 }
   2127 
   2128                 @Override
   2129                 public void onAnimationRepeat(Animator animation) {
   2130                     // Do nothing.
   2131                 }
   2132             });
   2133 
   2134             mZoomAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   2135                 @Override
   2136                 public void onAnimationUpdate(ValueAnimator animation) {
   2137                     float newScale = (Float) animation.getAnimatedValue();
   2138                     float postScale = newScale / mScale;
   2139                     mScale = newScale;
   2140                     current.postScale(focusX, focusY, postScale, mDrawArea.width(),
   2141                             mDrawArea.height());
   2142                 }
   2143             });
   2144             mZoomAnimator.start();
   2145         }
   2146 
   2147         @Override
   2148         public void scroll(float deltaX) {
   2149             if (!stopScrolling(false)) {
   2150                 return;
   2151             }
   2152             mCenterX += deltaX;
   2153 
   2154             boolean stopScroll = clampCenterX();
   2155             checkCurrentDataCentered(getCurrentId());
   2156             if (stopScroll) {
   2157                 mController.stopScrolling(true);
   2158             }
   2159             invalidate();
   2160         }
   2161 
   2162         @Override
   2163         public void fling(float velocityX) {
   2164             if (!stopScrolling(false)) {
   2165                 return;
   2166             }
   2167             ViewItem item = mViewItem[mCurrentItem];
   2168             if (item == null) {
   2169                 return;
   2170             }
   2171 
   2172             float scaledVelocityX = velocityX / mScale;
   2173             if (inFullScreen() && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY
   2174                     && scaledVelocityX < 0) {
   2175                 // Swipe left in camera preview.
   2176                 goToFilmStrip();
   2177             }
   2178 
   2179             int w = getWidth();
   2180             // Estimation of possible length on the left. To ensure the
   2181             // velocity doesn't become too slow eventually, we add a huge number
   2182             // to the estimated maximum.
   2183             int minX = estimateMinX(item.getId(), item.getLeftPosition(), w);
   2184             // Estimation of possible length on the right. Likewise, exaggerate
   2185             // the possible maximum too.
   2186             int maxX = estimateMaxX(item.getId(), item.getLeftPosition(), w);
   2187             mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0);
   2188         }
   2189 
   2190         @Override
   2191         public void flingInsideZoomView(float velocityX, float velocityY) {
   2192             if (!isZoomStarted()) {
   2193                 return;
   2194             }
   2195 
   2196             final ViewItem current = mViewItem[mCurrentItem];
   2197             if (current == null) {
   2198                 return;
   2199             }
   2200 
   2201             final int factor = DECELERATION_FACTOR;
   2202             // Deceleration curve for distance:
   2203             // S(t) = s + (e - s) * (1 - (1 - t/T) ^ factor)
   2204             // Need to find the ending distance (e), so that the starting velocity
   2205             // is the velocity of fling.
   2206             // Velocity is the derivative of distance
   2207             // V(t) = (e - s) * factor * (-1) * (1 - t/T) ^ (factor - 1) * (-1/T)
   2208             //      = (e - s) * factor * (1 - t/T) ^ (factor - 1) / T
   2209             // Since V(0) = V0, we have e = T / factor * V0 + s
   2210 
   2211             // Duration T should be long enough so that at the end of the fling,
   2212             // image moves at 1 pixel/s for about P = 50ms = 0.05s
   2213             // i.e. V(T - P) = 1
   2214             // V(T - P) = V0 * (1 - (T -P) /T) ^ (factor - 1) = 1
   2215             // T = P * V0 ^ (1 / (factor -1))
   2216 
   2217             final float velocity = Math.max(Math.abs(velocityX), Math.abs(velocityY));
   2218             // Dynamically calculate duration
   2219             final float duration = (float) (FLING_COASTING_DURATION_S
   2220                     * Math.pow(velocity, (1f/ (factor - 1f))));
   2221 
   2222             final float translationX = current.getTranslationX();
   2223             final float translationY = current.getTranslationY();
   2224 
   2225             final ValueAnimator decelerationX = ValueAnimator.ofFloat(translationX,
   2226                     translationX + duration / factor * velocityX);
   2227             final ValueAnimator decelerationY = ValueAnimator.ofFloat(translationY,
   2228                     translationY + duration / factor * velocityY);
   2229 
   2230             decelerationY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   2231                 @Override
   2232                 public void onAnimationUpdate(ValueAnimator animation) {
   2233                     float transX = (Float) decelerationX.getAnimatedValue();
   2234                     float transY = (Float) decelerationY.getAnimatedValue();
   2235 
   2236                     current.updateTransform(transX, transY, mScale,
   2237                             mScale, mDrawArea.width(), mDrawArea.height());
   2238                 }
   2239             });
   2240 
   2241             mFlingAnimator = new AnimatorSet();
   2242             mFlingAnimator.play(decelerationX).with(decelerationY);
   2243             mFlingAnimator.setDuration((int) (duration * 1000));
   2244             mFlingAnimator.setInterpolator(new TimeInterpolator() {
   2245                 @Override
   2246                 public float getInterpolation(float input) {
   2247                     return (float)(1.0f - Math.pow((1.0f - input), factor));
   2248                 }
   2249             });
   2250             mFlingAnimator.addListener(new Animator.AnimatorListener() {
   2251                 private boolean mCancelled = false;
   2252                 @Override
   2253                 public void onAnimationStart(Animator animation) {
   2254 
   2255                 }
   2256 
   2257                 @Override
   2258                 public void onAnimationEnd(Animator animation) {
   2259                     if (!mCancelled) {
   2260                         loadZoomedImage();
   2261                     }
   2262                     mFlingAnimator = null;
   2263                 }
   2264 
   2265                 @Override
   2266                 public void onAnimationCancel(Animator animation) {
   2267                     mCancelled = true;
   2268                 }
   2269 
   2270                 @Override
   2271                 public void onAnimationRepeat(Animator animation) {
   2272 
   2273                 }
   2274             });
   2275             mFlingAnimator.start();
   2276         }
   2277 
   2278         @Override
   2279         public boolean stopScrolling(boolean forced) {
   2280             if (!isScrolling()) {
   2281                 return true;
   2282             } else if (!mCanStopScroll && !forced) {
   2283                 return false;
   2284             }
   2285             mScroller.forceFinished(true);
   2286             return true;
   2287         }
   2288 
   2289         private void stopScale() {
   2290             mScaleAnimator.cancel();
   2291         }
   2292 
   2293         @Override
   2294         public void scrollToPosition(int position, int duration, boolean interruptible) {
   2295             if (mViewItem[mCurrentItem] == null) {
   2296                 return;
   2297             }
   2298             mCanStopScroll = interruptible;
   2299             mScroller.startScroll(mCenterX, 0, position - mCenterX,
   2300                     0, duration);
   2301 
   2302             checkCurrentDataCentered(mViewItem[mCurrentItem].getId());
   2303         }
   2304 
   2305         @Override
   2306         public boolean goToNextItem() {
   2307             final ViewItem nextItem = mViewItem[mCurrentItem + 1];
   2308             if (nextItem == null) {
   2309                 return false;
   2310             }
   2311             stopScrolling(true);
   2312             scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS * 2, false);
   2313 
   2314             if (getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   2315                 // Special case when moving from camera preview.
   2316                 scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
   2317             }
   2318             return true;
   2319         }
   2320 
   2321         private void scaleTo(float scale, int duration) {
   2322             if (mViewItem[mCurrentItem] == null) {
   2323                 return;
   2324             }
   2325             stopScale();
   2326             mScaleAnimator.setDuration(duration);
   2327             mScaleAnimator.setFloatValues(mScale, scale);
   2328             mScaleAnimator.start();
   2329         }
   2330 
   2331         @Override
   2332         public void goToFilmStrip() {
   2333             scaleTo(FILM_STRIP_SCALE, GEOMETRY_ADJUST_TIME_MS);
   2334 
   2335             final ViewItem nextItem = mViewItem[mCurrentItem + 1];
   2336             if (mViewItem[mCurrentItem].getId() == 0 &&
   2337                     getCurrentViewType() == ImageData.VIEW_TYPE_STICKY &&
   2338                     nextItem != null) {
   2339                 // Deal with the special case of swiping in camera preview.
   2340                 scrollToPosition(nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, false);
   2341             }
   2342 
   2343             if (mListener != null) {
   2344                 mListener.onDataFullScreenChange(mViewItem[mCurrentItem].getId(), false);
   2345             }
   2346         }
   2347 
   2348         @Override
   2349         public void goToFullScreen() {
   2350             if (inFullScreen()) {
   2351                 return;
   2352             }
   2353             enterFullScreen();
   2354             scaleTo(1f, GEOMETRY_ADJUST_TIME_MS);
   2355         }
   2356 
   2357         private void cancelFlingAnimation() {
   2358             // Cancels flinging for zoomed images
   2359             if (isFlingAnimationRunning()) {
   2360                 mFlingAnimator.cancel();
   2361             }
   2362         }
   2363 
   2364         private void cancelZoomAnimation() {
   2365             if (isZoomAnimationRunning()) {
   2366                 mZoomAnimator.cancel();
   2367             }
   2368         }
   2369 
   2370         private void enterFullScreen() {
   2371             if (mListener != null) {
   2372                 mListener.onDataFullScreenChange(mViewItem[mCurrentItem].getId(), true);
   2373             }
   2374         }
   2375 
   2376         private void setSurroundingViewsVisible(boolean visible) {
   2377             // Hide everything on the left
   2378             // TODO: Need to find a better way to toggle the visibility of views around
   2379             //       the current view.
   2380             for (int i = 0; i < mCurrentItem; i++) {
   2381                 if (i == mCurrentItem || mViewItem[i] == null) {
   2382                     continue;
   2383                 }
   2384                 mViewItem[i].getView().setVisibility(visible ? VISIBLE : INVISIBLE);
   2385             }
   2386         }
   2387 
   2388         private void leaveFullScreen() {
   2389             if (mListener != null) {
   2390                 mListener.onDataFullScreenChange(mViewItem[mCurrentItem].getId(), false);
   2391             }
   2392         }
   2393 
   2394         private Uri getCurrentContentUri() {
   2395             ViewItem curr = mViewItem[mCurrentItem];
   2396             if (curr == null) {
   2397                 return Uri.EMPTY;
   2398             }
   2399             return mDataAdapter.getImageData(curr.getId()).getContentUri();
   2400         }
   2401 
   2402         /**
   2403          * Here we only support up to 1:1 image zoom (i.e. a 100% view of the
   2404          * actual pixels). The max scale that we can apply on the view should
   2405          * make the view same size as the image, in pixels.
   2406          */
   2407         private float getCurrentDataMaxScale(boolean allowOverScale) {
   2408             ViewItem curr = mViewItem[mCurrentItem];
   2409             ImageData imageData = mDataAdapter.getImageData(curr.getId());
   2410             if (curr == null || !imageData
   2411                     .isUIActionSupported(ImageData.ACTION_ZOOM)) {
   2412                 return FULL_SCREEN_SCALE;
   2413             }
   2414             float imageWidth = imageData.getWidth();
   2415             if (imageData.getOrientation() == 90 || imageData.getOrientation() == 270) {
   2416                 imageWidth = imageData.getHeight();
   2417             }
   2418             float scale = imageWidth / curr.getWidth();
   2419             if (allowOverScale) {
   2420                 // In addition to the scale we apply to the view for 100% view
   2421                 // (i.e. each pixel on screen corresponds to a pixel in image)
   2422                 // we allow scaling beyond that for better detail viewing.
   2423                 scale *= mOverScaleFactor;
   2424             }
   2425             return scale;
   2426         }
   2427 
   2428         private void loadZoomedImage() {
   2429             if (!isZoomStarted()) {
   2430                 return;
   2431             }
   2432             ViewItem curr = mViewItem[mCurrentItem];
   2433             if (curr == null) {
   2434                 return;
   2435             }
   2436             ImageData imageData = mDataAdapter.getImageData(curr.getId());
   2437             if(!imageData.isUIActionSupported(ImageData.ACTION_ZOOM)) {
   2438                 return;
   2439             }
   2440             Uri uri = getCurrentContentUri();
   2441             RectF viewRect = curr.getViewRect();
   2442             if (uri == null || uri == Uri.EMPTY) {
   2443                 return;
   2444             }
   2445             int orientation = imageData.getOrientation();
   2446             mZoomView.loadBitmap(uri, orientation, viewRect);
   2447         }
   2448 
   2449         private void cancelLoadingZoomedImage() {
   2450             mZoomView.cancelPartialDecodingTask();
   2451         }
   2452 
   2453         @Override
   2454         public void goToFirstItem() {
   2455             resetZoomView();
   2456             // TODO: animate to camera if it is still in the mViewItem buffer
   2457             // versus a full reload which will perform an immediate transition
   2458             reload();
   2459         }
   2460 
   2461         public boolean isZoomStarted() {
   2462             return mScale > FULL_SCREEN_SCALE;
   2463         }
   2464 
   2465         public boolean isFlingAnimationRunning() {
   2466             return mFlingAnimator != null && mFlingAnimator.isRunning();
   2467         }
   2468 
   2469         public boolean isZoomAnimationRunning() {
   2470             return mZoomAnimator != null && mZoomAnimator.isRunning();
   2471         }
   2472     }
   2473 
   2474     private static class MyScroller {
   2475         public interface Listener {
   2476             public void onScrollUpdate(int currX, int currY);
   2477             public void onScrollEnd();
   2478         }
   2479 
   2480         private Handler mHandler;
   2481         private Listener mListener;
   2482 
   2483         private final Scroller mScroller;
   2484 
   2485         private final ValueAnimator mXScrollAnimator;
   2486         private Runnable mScrollChecker = new Runnable() {
   2487             @Override
   2488             public void run() {
   2489                 boolean newPosition = mScroller.computeScrollOffset();
   2490                 if (!newPosition) {
   2491                     mListener.onScrollEnd();
   2492                     return;
   2493                 }
   2494                 mListener.onScrollUpdate(mScroller.getCurrX(), mScroller.getCurrY());
   2495                 mHandler.removeCallbacks(this);
   2496                 mHandler.post(this);
   2497             }
   2498         };
   2499 
   2500         private ValueAnimator.AnimatorUpdateListener mXScrollAnimatorUpdateListener =
   2501                 new ValueAnimator.AnimatorUpdateListener() {
   2502                     @Override
   2503                     public void onAnimationUpdate(ValueAnimator animation) {
   2504                         mListener.onScrollUpdate((Integer) animation.getAnimatedValue(), 0);
   2505                     }
   2506                 };
   2507 
   2508         private Animator.AnimatorListener mXScrollAnimatorListener =
   2509                 new Animator.AnimatorListener() {
   2510                     @Override
   2511                     public void onAnimationCancel(Animator animation) {
   2512                         // Do nothing.
   2513                     }
   2514 
   2515                     @Override
   2516                     public void onAnimationEnd(Animator animation) {
   2517                         mListener.onScrollEnd();
   2518                     }
   2519 
   2520                     @Override
   2521                     public void onAnimationRepeat(Animator animation) {
   2522                         // Do nothing.
   2523                     }
   2524 
   2525                     @Override
   2526                     public void onAnimationStart(Animator animation) {
   2527                         // Do nothing.
   2528                     }
   2529                 };
   2530 
   2531 
   2532         public MyScroller(Context ctx, Handler handler, Listener listener,
   2533                 TimeInterpolator interpolator) {
   2534             mHandler = handler;
   2535             mListener = listener;
   2536             mScroller = new Scroller(ctx);
   2537             mXScrollAnimator = new ValueAnimator();
   2538             mXScrollAnimator.addUpdateListener(mXScrollAnimatorUpdateListener);
   2539             mXScrollAnimator.addListener(mXScrollAnimatorListener);
   2540             mXScrollAnimator.setInterpolator(interpolator);
   2541         }
   2542 
   2543         public void fling(
   2544                 int startX, int startY,
   2545                 int velocityX, int velocityY,
   2546                 int minX, int maxX,
   2547                 int minY, int maxY) {
   2548             mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
   2549             runChecker();
   2550         }
   2551 
   2552         public void startScroll(int startX, int startY, int dx, int dy) {
   2553             mScroller.startScroll(startX, startY, dx, dy);
   2554             runChecker();
   2555         }
   2556 
   2557         /** Only starts and updates scroll in x-axis. */
   2558         public void startScroll(int startX, int startY, int dx, int dy, int duration) {
   2559             mXScrollAnimator.cancel();
   2560             mXScrollAnimator.setDuration(duration);
   2561             mXScrollAnimator.setIntValues(startX, startX + dx);
   2562             mXScrollAnimator.start();
   2563         }
   2564 
   2565         public boolean isFinished() {
   2566             return (mScroller.isFinished() && !mXScrollAnimator.isRunning());
   2567         }
   2568 
   2569         public void forceFinished(boolean finished) {
   2570             mScroller.forceFinished(finished);
   2571             if (finished) {
   2572                 mXScrollAnimator.cancel();
   2573             }
   2574         }
   2575 
   2576         private void runChecker() {
   2577             if (mHandler == null || mListener == null) {
   2578                 return;
   2579             }
   2580             mHandler.removeCallbacks(mScrollChecker);
   2581             mHandler.post(mScrollChecker);
   2582         }
   2583     }
   2584 
   2585     private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener {
   2586         // Indicating the current trend of scaling is up (>1) or down (<1).
   2587         private float mScaleTrend;
   2588         private float mMaxScale;
   2589 
   2590         @Override
   2591         public boolean onSingleTapUp(float x, float y) {
   2592             ViewItem centerItem = mViewItem[mCurrentItem];
   2593             if (inFilmStrip()) {
   2594                 if (centerItem != null && centerItem.areaContains(x, y)) {
   2595                     mController.goToFullScreen();
   2596                     return true;
   2597                 }
   2598             } else if (inFullScreen()) {
   2599                 int dataID = -1;
   2600                 if (centerItem != null) {
   2601                     dataID = centerItem.getId();
   2602                 }
   2603                 mListener.onToggleSystemDecorsVisibility(dataID);
   2604                 return true;
   2605             }
   2606             return false;
   2607         }
   2608 
   2609         @Override
   2610         public boolean onDoubleTap(float x, float y) {
   2611             ViewItem current = mViewItem[mCurrentItem];
   2612             if (inFilmStrip() && current != null) {
   2613                 mController.goToFullScreen();
   2614                 return true;
   2615             } else if (mScale < FULL_SCREEN_SCALE || inCameraFullscreen()) {
   2616                 return false;
   2617             }
   2618             if (current == null) {
   2619                 return false;
   2620             }
   2621             if (!mController.stopScrolling(false)) {
   2622                 return false;
   2623             }
   2624             mListener.setSystemDecorsVisibility(false);
   2625             mController.zoomAt(current, x, y);
   2626             return true;
   2627         }
   2628 
   2629         @Override
   2630         public boolean onDown(float x, float y) {
   2631             mController.cancelFlingAnimation();
   2632             if (!mController.stopScrolling(false)) {
   2633                 return false;
   2634             }
   2635 
   2636             return true;
   2637         }
   2638 
   2639         @Override
   2640         public boolean onUp(float x, float y) {
   2641             final ViewItem currItem = mViewItem[mCurrentItem];
   2642             if (currItem == null) {
   2643                 return false;
   2644             }
   2645             if (mController.isZoomAnimationRunning() || mController.isFlingAnimationRunning()) {
   2646                 return false;
   2647             }
   2648             if (mController.isZoomStarted()) {
   2649                 mController.loadZoomedImage();
   2650                 return true;
   2651             }
   2652             float halfH = getHeight() / 2;
   2653             mIsUserScrolling = false;
   2654             // Finds items promoted/demoted.
   2655             for (int i = 0; i < BUFFER_SIZE; i++) {
   2656                 if (mViewItem[i] == null) {
   2657                     continue;
   2658                 }
   2659                 float transY = mViewItem[i].getScaledTranslationY(mScale);
   2660                 if (transY == 0) {
   2661                     continue;
   2662                 }
   2663                 int id = mViewItem[i].getId();
   2664 
   2665                 if (mDataAdapter.getImageData(id)
   2666                         .isUIActionSupported(ImageData.ACTION_DEMOTE)
   2667                         && transY > halfH) {
   2668                     demoteData(i, id);
   2669                 } else if (mDataAdapter.getImageData(id)
   2670                         .isUIActionSupported(ImageData.ACTION_PROMOTE)
   2671                         && transY < -halfH) {
   2672                     promoteData(i, id);
   2673                 } else {
   2674                     // put the view back.
   2675                     mViewItem[i].getView().animate()
   2676                             .translationY(0f)
   2677                             .alpha(1f)
   2678                             .setDuration(GEOMETRY_ADJUST_TIME_MS)
   2679                             .start();
   2680                 }
   2681             }
   2682 
   2683             int currId = currItem.getId();
   2684             if (mCenterX > currItem.getCenterX() + CAMERA_PREVIEW_SWIPE_THRESHOLD
   2685                     && currId == 0
   2686                     && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY
   2687                     && mDataIdOnUserScrolling == 0) {
   2688                 mController.goToFilmStrip();
   2689                 // Special case to go from camera preview to the next photo.
   2690                 if (mViewItem[mCurrentItem + 1] != null) {
   2691                     mController.scrollToPosition(
   2692                             mViewItem[mCurrentItem + 1].getCenterX(),
   2693                             GEOMETRY_ADJUST_TIME_MS, false);
   2694                 } else {
   2695                     // No next photo.
   2696                     snapInCenter();
   2697                 }
   2698             } if (mCenterX == currItem.getCenterX() && currId == 0
   2699                     && getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   2700                 mController.goToFullScreen();
   2701             } else {
   2702                 if (mDataIdOnUserScrolling  == 0 && currId != 0) {
   2703                     // Special case to go to filmstrip when the user scroll away
   2704                     // from the camera preview and the current one is not the
   2705                     // preview anymore.
   2706                     mController.goToFilmStrip();
   2707                     mDataIdOnUserScrolling = currId;
   2708                 }
   2709                 snapInCenter();
   2710             }
   2711             return false;
   2712         }
   2713 
   2714         @Override
   2715         public boolean onScroll(float x, float y, float dx, float dy) {
   2716             ViewItem currItem = mViewItem[mCurrentItem];
   2717             if (currItem == null) {
   2718                 return false;
   2719             }
   2720             if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
   2721                 return false;
   2722             }
   2723             hideZoomView();
   2724             // When image is zoomed in to be bigger than the screen
   2725             if (mController.isZoomStarted()) {
   2726                 ViewItem curr = mViewItem[mCurrentItem];
   2727                 float transX = curr.getTranslationX() - dx;
   2728                 float transY = curr.getTranslationY() - dy;
   2729                 curr.updateTransform(transX, transY, mScale, mScale, mDrawArea.width(),
   2730                         mDrawArea.height());
   2731                 return true;
   2732             }
   2733             int deltaX = (int) (dx / mScale);
   2734             // Forces the current scrolling to stop.
   2735             mController.stopScrolling(true);
   2736             if (!mIsUserScrolling) {
   2737                 mIsUserScrolling = true;
   2738                 mDataIdOnUserScrolling = mViewItem[mCurrentItem].getId();
   2739             }
   2740             if (inFilmStrip()) {
   2741                 if (Math.abs(dx) > Math.abs(dy)) {
   2742                     mController.scroll(deltaX);
   2743                 } else {
   2744                     // Vertical part. Promote or demote.
   2745                     int hit = 0;
   2746                     Rect hitRect = new Rect();
   2747                     for (; hit < BUFFER_SIZE; hit++) {
   2748                         if (mViewItem[hit] == null) {
   2749                             continue;
   2750                         }
   2751                         mViewItem[hit].getView().getHitRect(hitRect);
   2752                         if (hitRect.contains((int) x, (int) y)) {
   2753                             break;
   2754                         }
   2755                     }
   2756                     if (hit == BUFFER_SIZE) {
   2757                         return false;
   2758                     }
   2759 
   2760                     ImageData data = mDataAdapter.getImageData(mViewItem[hit].getId());
   2761                     float transY = mViewItem[hit].getScaledTranslationY(mScale) - dy / mScale;
   2762                     if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) {
   2763                         transY = 0f;
   2764                     }
   2765                     if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) {
   2766                         transY = 0f;
   2767                     }
   2768                     mViewItem[hit].setTranslationY(transY, mScale);
   2769                 }
   2770             } else if (inFullScreen()) {
   2771                 // Multiplied by 1.2 to make it more easy to swipe.
   2772                 mController.scroll((int) (deltaX * 1.2));
   2773             }
   2774             invalidate();
   2775 
   2776             return true;
   2777         }
   2778 
   2779         @Override
   2780         public boolean onFling(float velocityX, float velocityY) {
   2781             final ViewItem currItem = mViewItem[mCurrentItem];
   2782             if (currItem == null) {
   2783                 return false;
   2784             }
   2785             if (!mDataAdapter.canSwipeInFullScreen(currItem.getId())) {
   2786                 return false;
   2787             }
   2788             if (mController.isZoomStarted()) {
   2789                 // Fling within the zoomed image
   2790                 mController.flingInsideZoomView(velocityX, velocityY);
   2791                 return true;
   2792             }
   2793             if (Math.abs(velocityX) < Math.abs(velocityY)) {
   2794                 // ignore vertical fling.
   2795                 return true;
   2796             }
   2797 
   2798             // In full-screen, fling of a velocity above a threshold should go to
   2799             // the next/prev photos
   2800             if (mScale == FULL_SCREEN_SCALE) {
   2801                 int currItemCenterX = currItem.getCenterX();
   2802 
   2803                 if (velocityX > 0) {  // left
   2804                     if (mCenterX > currItemCenterX) {
   2805                         // The visually previous item is actually the current item.
   2806                         mController.scrollToPosition(
   2807                                 currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
   2808                         return true;
   2809                     }
   2810                     ViewItem prevItem = mViewItem[mCurrentItem - 1];
   2811                     if (prevItem == null) {
   2812                         return false;
   2813                     }
   2814                     mController.scrollToPosition(
   2815                             prevItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
   2816                 } else {  // right
   2817                     if (mController.stopScrolling(false)) {
   2818                         if (mCenterX < currItemCenterX) {
   2819                             // The visually next item is actually the current item.
   2820                             mController.scrollToPosition(
   2821                                     currItemCenterX, GEOMETRY_ADJUST_TIME_MS, true);
   2822                             return true;
   2823                         }
   2824                         final ViewItem nextItem = mViewItem[mCurrentItem + 1];
   2825                         if (nextItem == null) {
   2826                             return false;
   2827                         }
   2828                         mController.scrollToPosition(
   2829                                 nextItem.getCenterX(), GEOMETRY_ADJUST_TIME_MS, true);
   2830                         if (getCurrentViewType() == ImageData.VIEW_TYPE_STICKY) {
   2831                             mController.goToFilmStrip();
   2832                         }
   2833                     }
   2834                 }
   2835             }
   2836 
   2837             if (mScale == FILM_STRIP_SCALE) {
   2838                 mController.fling(velocityX);
   2839             }
   2840             return true;
   2841         }
   2842 
   2843         @Override
   2844         public boolean onScaleBegin(float focusX, float focusY) {
   2845             if (inCameraFullscreen()) {
   2846                 return false;
   2847             }
   2848 
   2849             hideZoomView();
   2850             mScaleTrend = 1f;
   2851             // If the image is smaller than screen size, we should allow to zoom
   2852             // in to full screen size
   2853             mMaxScale = Math.max(mController.getCurrentDataMaxScale(true), FULL_SCREEN_SCALE);
   2854             return true;
   2855         }
   2856 
   2857         @Override
   2858         public boolean onScale(float focusX, float focusY, float scale) {
   2859             if (inCameraFullscreen()) {
   2860                 return false;
   2861             }
   2862 
   2863             mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f;
   2864             float newScale = mScale * scale;
   2865             if (mScale < FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
   2866                 // Scaled view is smaller than or equal to screen size both before
   2867                 // and after scaling
   2868                 mScale = newScale;
   2869                 if (mScale <= FILM_STRIP_SCALE) {
   2870                     mScale = FILM_STRIP_SCALE;
   2871                 }
   2872                 invalidate();
   2873             } else if (mScale < FULL_SCREEN_SCALE && newScale >= FULL_SCREEN_SCALE) {
   2874                 // Going from smaller than screen size to bigger than or equal to screen size
   2875                 mScale = FULL_SCREEN_SCALE;
   2876                 mController.enterFullScreen();
   2877                 invalidate();
   2878                 mController.setSurroundingViewsVisible(false);
   2879             } else if (mScale >= FULL_SCREEN_SCALE && newScale < FULL_SCREEN_SCALE) {
   2880                 // Going from bigger than or equal to screen size to smaller than screen size
   2881                 mScale = newScale;
   2882                 mController.leaveFullScreen();
   2883                 invalidate();
   2884                 mController.setSurroundingViewsVisible(true);
   2885             } else {
   2886                 // Scaled view bigger than or equal to screen size both before
   2887                 // and after scaling
   2888                 if (!mController.isZoomStarted()) {
   2889                     mController.setSurroundingViewsVisible(false);
   2890                 }
   2891                 ViewItem curr = mViewItem[mCurrentItem];
   2892                 // Make sure the image is not overly scaled
   2893                 newScale = Math.min(newScale, mMaxScale);
   2894                 if (newScale == mScale) {
   2895                     return true;
   2896                 }
   2897                 float postScale = newScale / mScale;
   2898                 curr.postScale(focusX, focusY, postScale, mDrawArea.width(), mDrawArea.height());
   2899                 mScale = newScale;
   2900             }
   2901             return true;
   2902         }
   2903 
   2904         @Override
   2905         public void onScaleEnd() {
   2906             if (mScale > FULL_SCREEN_SCALE + TOLERANCE) {
   2907                 return;
   2908             }
   2909             mController.setSurroundingViewsVisible(true);
   2910             if (mScale <= FILM_STRIP_SCALE + TOLERANCE) {
   2911                 mController.goToFilmStrip();
   2912             } else if (mScaleTrend > 1f || mScale > FULL_SCREEN_SCALE - TOLERANCE) {
   2913                 if (mController.isZoomStarted()) {
   2914                     mScale = FULL_SCREEN_SCALE;
   2915                     resetZoomView();
   2916                 }
   2917                 mController.goToFullScreen();
   2918             } else {
   2919                 mController.goToFilmStrip();
   2920             }
   2921             mScaleTrend = 1f;
   2922         }
   2923     }
   2924 }
   2925