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