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