OSDN Git Service

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