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