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