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