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