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