OSDN Git Service

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