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