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