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