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