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