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