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