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