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