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