OSDN Git Service

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