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