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