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