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