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