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