OSDN Git Service

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