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