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