OSDN Git Service

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