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          */
PlayVideoIntent(CameraActivity activity)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
playVideo(Uri uri, String title)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          */
ViewItem(int index, View v, FilmstripItem data, FilmstripView filmstrip)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 
getData()194         public FilmstripItem getData() {
195             return mData;
196         }
197 
setData(FilmstripItem item)198         public void setData(FilmstripItem item) {
199             mData = item;
200 
201             renderTiny();
202         }
203 
renderTiny()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 
renderThumbnail()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 
renderFullRes()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 
lockAtFullOpacity()231         public void lockAtFullOpacity() {
232             if (!mLockAtFullOpacity) {
233                 mLockAtFullOpacity = true;
234                 mView.setAlpha(1.0f);
235             }
236         }
237 
unlockOpacity()238         public void unlockOpacity() {
239             mLockAtFullOpacity = false;
240         }
241 
242         /**
243          * Returns the index from
244          * {@link com.android.camera.filmstrip.FilmstripDataAdapter}.
245          */
getAdapterIndex()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          */
setIndex(int index)254         public void setIndex(int index) {
255             mIndex = index;
256         }
257 
258         /** Sets the left position of the view in the whole filmstrip. */
setLeftPosition(int pos)259         public void setLeftPosition(int pos) {
260             mLeftPosition = pos;
261         }
262 
263         /** Returns the left position of the view in the whole filmstrip. */
getLeftPosition()264         public int getLeftPosition() {
265             return mLeftPosition;
266         }
267 
268         /** Returns the translation of Y regarding the view scale. */
getTranslationY()269         public float getTranslationY() {
270             return mView.getTranslationY() / mFilmstrip.mScale;
271         }
272 
273         /** Returns the translation of X regarding the view scale. */
getTranslationX()274         public float getTranslationX() {
275             return mView.getTranslationX() / mFilmstrip.mScale;
276         }
277 
278         /** Sets the translation of Y regarding the view scale. */
setTranslationY(float transY)279         public void setTranslationY(float transY) {
280             mView.setTranslationY(transY * mFilmstrip.mScale);
281         }
282 
283         /** Sets the translation of X regarding the view scale. */
setTranslationX(float transX)284         public void setTranslationX(float transX) {
285             mView.setTranslationX(transX * mFilmstrip.mScale);
286         }
287 
288         /** Forwarding of {@link android.view.View#setAlpha(float)}. */
setAlpha(float alpha)289         public void setAlpha(float alpha) {
290             if (!mLockAtFullOpacity) {
291                 mView.setAlpha(alpha);
292             }
293         }
294 
295         /** Forwarding of {@link android.view.View#getAlpha()}. */
getAlpha()296         public float getAlpha() {
297             return mView.getAlpha();
298         }
299 
300         /** Forwarding of {@link android.view.View#getMeasuredWidth()}. */
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          */
animateTranslationX( float targetX, long duration_ms, TimeInterpolator interpolator)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          */
animateTranslationY( float targetY, long duration_ms, TimeInterpolator interpolator)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          */
animateAlpha(float targetAlpha, long duration_ms, TimeInterpolator interpolator)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 
runAnimation(final ValueAnimator animator, final float startValue, final float targetValue, final long duration_ms, final TimeInterpolator interpolator)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. */
translateXScaledBy(float transX)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          */
getHitRect(Rect rect)398         public void getHitRect(Rect rect) {
399             mView.getHitRect(rect);
400         }
401 
getCenterX()402         public int getCenterX() {
403             return mLeftPosition + mView.getMeasuredWidth() / 2;
404         }
405 
406         /** Forwarding of {@link android.view.View#getVisibility()}. */
getVisibility()407         public int getVisibility() {
408             return mView.getVisibility();
409         }
410 
411         /** Forwarding of {@link android.view.View#setVisibility(int)}. */
setVisibility(int visibility)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          */
addViewToHierarchy()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          */
removeViewFromHierarchy()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          */
bringViewToFront()445         public void bringViewToFront() {
446             mFilmstrip.bringChildToFront(mView);
447         }
448 
449         /**
450          * The visual x position of this view, in pixels.
451          */
getX()452         public float getX() {
453             return mView.getX();
454         }
455 
456         /**
457          * The visual y position of this view, in pixels.
458          */
getY()459         public float getY() {
460             return mView.getY();
461         }
462 
463         /**
464          * Forwarding of {@link android.view.View#measure(int, int)}.
465          */
measure(int widthSpec, int heightSpec)466         public void measure(int widthSpec, int heightSpec) {
467             mView.measure(widthSpec, heightSpec);
468         }
469 
layoutAt(int left, int top)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          */
getViewRect()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 
getView()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          */
layoutWithTranslationX(Rect drawArea, int refCenter, float scale)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. */
areaContains(float x, float y)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          */
getWidth()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          */
getDrawAreaLeft()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          */
postScale(float focusX, float focusY, float postScale, int viewportWidth, int viewportHeight)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 
updateTransform(float transX, float transY, float scaleX, float scaleY, int viewportWidth, int viewportHeight)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 
resetTransform()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
toString()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. */
FilmstripView(Context context)593     public FilmstripView(Context context) {
594         super(context);
595         init((CameraActivity) context);
596     }
597 
598     /** Constructor. */
FilmstripView(Context context, AttributeSet attrs)599     public FilmstripView(Context context, AttributeSet attrs) {
600         super(context, attrs);
601         init((CameraActivity) context);
602     }
603 
604     /** Constructor. */
FilmstripView(Context context, AttributeSet attrs, int defStyle)605     public FilmstripView(Context context, AttributeSet attrs, int defStyle) {
606         super(context, attrs, defStyle);
607         init((CameraActivity) context);
608     }
609 
init(CameraActivity cameraActivity)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 
recycleView(View view, int index)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 
getRecycledView(int index)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      */
getController()710     public FilmstripController getController() {
711         return mController;
712     }
713 
714     /**
715      * Returns the draw area width of the current item.
716      */
getCurrentItemLeft()717     public int  getCurrentItemLeft() {
718         return mViewItems[BUFFER_CENTER].getDrawAreaLeft();
719     }
720 
setListener(FilmstripController.FilmstripListener l)721     private void setListener(FilmstripController.FilmstripListener l) {
722         mListener = l;
723     }
724 
setViewGap(int viewGap)725     private void setViewGap(int viewGap) {
726         mViewGapInPixel = viewGap;
727     }
728 
729     /**
730      * Called after current item or zoom level has changed.
731      */
zoomAtIndexChanged()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      */
isItemAtIndexCentered(int index)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 
measureViewItem(ViewItem item, int boundWidth, int boundHeight)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
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 
findTheNearestView(int viewX)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 
buildViewItemAt(int index)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 
renderFullRes(int bufferIndex)859     private void renderFullRes(int bufferIndex) {
860         ViewItem item = mViewItems[bufferIndex];
861         if (item == null) {
862             return;
863         }
864 
865         item.renderFullRes();
866     }
867 
renderThumbnail(int bufferIndex)868     private void renderThumbnail(int bufferIndex) {
869         ViewItem item = mViewItems[bufferIndex];
870         if (item == null) {
871             return;
872         }
873 
874         item.renderThumbnail();
875     }
876 
renderAllThumbnails()877     private void renderAllThumbnails() {
878         for(int i = 0; i < BUFFER_SIZE; i++) {
879             renderThumbnail(i);
880         }
881     }
882 
removeItem(int bufferIndex)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      */
stepIfNeeded()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      */
clampCenterX()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      */
adjustChildZOrder()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      */
getCurrentItemAdapterIndex()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      */
scrollCurrentItemToCenter()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      */
translateLeftViewItem( int index, int drawAreaWidth, float scaleFraction)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      */
fadeAndScaleRightViewItem(int bufferIndex)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 
layoutViewItems(boolean layoutChanged)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
onDraw(Canvas c)1277     public void onDraw(Canvas c) {
1278         // TODO: remove layoutViewItems() here.
1279         layoutViewItems(false);
1280         super.onDraw(c);
1281     }
1282 
1283     @Override
onLayout(boolean changed, int l, int t, int r, int b)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      */
resetZoomView()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 
hideZoomView()1322     private void hideZoomView() {
1323         if (inZoomView()) {
1324             mController.cancelLoadingZoomedImage();
1325             mZoomView.setVisibility(GONE);
1326         }
1327     }
1328 
slideViewBack(ViewItem item)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 
animateItemRemoval(int index)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.
findItemInBufferByAdapterIndex(int index)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 
updateInsertion(int index)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 
setDataAdapter(FilmstripDataAdapter adapter)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 
inFilmstrip()1599     private boolean inFilmstrip() {
1600         return (mScale == FILM_STRIP_SCALE);
1601     }
1602 
inFullScreen()1603     private boolean inFullScreen() {
1604         return (mScale == FULL_SCREEN_SCALE);
1605     }
1606 
inZoomView()1607     private boolean inZoomView() {
1608         return (mScale > FULL_SCREEN_SCALE);
1609     }
1610 
1611     @Override
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)1646     public boolean onTouchEvent(MotionEvent ev) {
1647         return mGestureRecognizer.onTouchEvent(ev);
1648     }
1649 
1650     @Override
onGenericMotionEvent(MotionEvent ev)1651     public boolean onGenericMotionEvent(MotionEvent ev) {
1652         mGestureRecognizer.onGenericMotionEvent(ev);
1653         return true;
1654     }
1655 
getGestureListener()1656     FilmstripGestureRecognizer.Listener getGestureListener() {
1657         return mGestureListener;
1658     }
1659 
updateViewItem(int bufferIndex)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. */
update(FilmstripDataAdapter.UpdateReporter reporter)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      */
reload()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 
promoteData(int index)1847     private void promoteData(int index) {
1848         if (mListener != null) {
1849             mListener.onFocusedDataPromoted(index);
1850         }
1851     }
1852 
demoteData(int index)1853     private void demoteData(int index) {
1854         if (mListener != null) {
1855             mListener.onFocusedDataDemoted(index);
1856         }
1857     }
1858 
onEnterFilmstrip()1859     private void onEnterFilmstrip() {
1860         Log.d(TAG, "onEnterFilmstrip()");
1861         if (mListener != null) {
1862             mListener.onEnterFilmstrip(getCurrentItemAdapterIndex());
1863         }
1864     }
1865 
onLeaveFilmstrip()1866     private void onLeaveFilmstrip() {
1867         if (mListener != null) {
1868             mListener.onLeaveFilmstrip(getCurrentItemAdapterIndex());
1869         }
1870     }
1871 
onEnterFullScreen()1872     private void onEnterFullScreen() {
1873         mFullScreenUIHidden = false;
1874         if (mListener != null) {
1875             mListener.onEnterFullScreenUiShown(getCurrentItemAdapterIndex());
1876         }
1877     }
1878 
onLeaveFullScreen()1879     private void onLeaveFullScreen() {
1880         if (mListener != null) {
1881             mListener.onLeaveFullScreenUiShown(getCurrentItemAdapterIndex());
1882         }
1883     }
1884 
onEnterFullScreenUiHidden()1885     private void onEnterFullScreenUiHidden() {
1886         mFullScreenUIHidden = true;
1887         if (mListener != null) {
1888             mListener.onEnterFullScreenUiHidden(getCurrentItemAdapterIndex());
1889         }
1890     }
1891 
onLeaveFullScreenUiHidden()1892     private void onLeaveFullScreenUiHidden() {
1893         mFullScreenUIHidden = false;
1894         if (mListener != null) {
1895             mListener.onLeaveFullScreenUiHidden(getCurrentItemAdapterIndex());
1896         }
1897     }
1898 
onEnterZoomView()1899     private void onEnterZoomView() {
1900         if (mListener != null) {
1901             mListener.onEnterZoomView(getCurrentItemAdapterIndex());
1902         }
1903     }
1904 
onLeaveZoomView()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 
FilmstripControllerImpl()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
setImageGap(int imageGap)2024         public void setImageGap(int imageGap) {
2025             FilmstripView.this.setViewGap(imageGap);
2026         }
2027 
2028         @Override
getCurrentAdapterIndex()2029         public int getCurrentAdapterIndex() {
2030             return FilmstripView.this.getCurrentItemAdapterIndex();
2031         }
2032 
2033         @Override
setDataAdapter(FilmstripDataAdapter adapter)2034         public void setDataAdapter(FilmstripDataAdapter adapter) {
2035             FilmstripView.this.setDataAdapter(adapter);
2036         }
2037 
2038         @Override
inFilmstrip()2039         public boolean inFilmstrip() {
2040             return FilmstripView.this.inFilmstrip();
2041         }
2042 
2043         @Override
inFullScreen()2044         public boolean inFullScreen() {
2045             return FilmstripView.this.inFullScreen();
2046         }
2047 
2048         @Override
setListener(FilmstripListener listener)2049         public void setListener(FilmstripListener listener) {
2050             FilmstripView.this.setListener(listener);
2051         }
2052 
2053         @Override
isScrolling()2054         public boolean isScrolling() {
2055             return !mScrollGesture.isFinished();
2056         }
2057 
2058         @Override
isScaling()2059         public boolean isScaling() {
2060             return mScaleAnimator.isRunning();
2061         }
2062 
estimateMinX(int index, int leftPos, int viewWidth)2063         private int estimateMinX(int index, int leftPos, int viewWidth) {
2064             return leftPos - (index + 100) * (viewWidth + mViewGapInPixel);
2065         }
2066 
estimateMaxX(int index, int leftPos, int viewWidth)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. */
zoomAt(final ViewItem current, final float focusX, final float focusY)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