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