1/*
2 * Copyright (C) 2010 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.gallery3d.ui;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.graphics.Color;
22import android.graphics.Matrix;
23import android.graphics.Rect;
24import android.os.Build;
25import android.os.Message;
26import android.util.FloatMath;
27import android.view.MotionEvent;
28import android.view.View.MeasureSpec;
29import android.view.animation.AccelerateInterpolator;
30
31import com.android.gallery3d.R;
32import com.android.gallery3d.app.AbstractGalleryActivity;
33import com.android.gallery3d.common.ApiHelper;
34import com.android.gallery3d.common.Utils;
35import com.android.gallery3d.data.MediaItem;
36import com.android.gallery3d.data.MediaObject;
37import com.android.gallery3d.data.Path;
38import com.android.gallery3d.glrenderer.GLCanvas;
39import com.android.gallery3d.glrenderer.RawTexture;
40import com.android.gallery3d.glrenderer.ResourceTexture;
41import com.android.gallery3d.glrenderer.StringTexture;
42import com.android.gallery3d.glrenderer.Texture;
43import com.android.gallery3d.util.GalleryUtils;
44import com.android.gallery3d.util.RangeArray;
45import com.android.gallery3d.util.UsageStatistics;
46
47public class PhotoView extends GLView {
48    @SuppressWarnings("unused")
49    private static final String TAG = "PhotoView";
50    private final int mPlaceholderColor;
51
52    public static final int INVALID_SIZE = -1;
53    public static final long INVALID_DATA_VERSION =
54            MediaObject.INVALID_DATA_VERSION;
55
56    public static class Size {
57        public int width;
58        public int height;
59    }
60
61    public interface Model extends TileImageView.TileSource {
62        public int getCurrentIndex();
63        public void moveTo(int index);
64
65        // Returns the size for the specified picture. If the size information is
66        // not avaiable, width = height = 0.
67        public void getImageSize(int offset, Size size);
68
69        // Returns the media item for the specified picture.
70        public MediaItem getMediaItem(int offset);
71
72        // Returns the rotation for the specified picture.
73        public int getImageRotation(int offset);
74
75        // This amends the getScreenNail() method of TileImageView.Model to get
76        // ScreenNail at previous (negative offset) or next (positive offset)
77        // positions. Returns null if the specified ScreenNail is unavailable.
78        public ScreenNail getScreenNail(int offset);
79
80        // Set this to true if we need the model to provide full images.
81        public void setNeedFullImage(boolean enabled);
82
83        // Returns true if the item is the Camera preview.
84        public boolean isCamera(int offset);
85
86        // Returns true if the item is the Panorama.
87        public boolean isPanorama(int offset);
88
89        // Returns true if the item is a static image that represents camera
90        // preview.
91        public boolean isStaticCamera(int offset);
92
93        // Returns true if the item is a Video.
94        public boolean isVideo(int offset);
95
96        // Returns true if the item can be deleted.
97        public boolean isDeletable(int offset);
98
99        public static final int LOADING_INIT = 0;
100        public static final int LOADING_COMPLETE = 1;
101        public static final int LOADING_FAIL = 2;
102
103        public int getLoadingState(int offset);
104
105        // When data change happens, we need to decide which MediaItem to focus
106        // on.
107        //
108        // 1. If focus hint path != null, we try to focus on it if we can find
109        // it.  This is used for undo a deletion, so we can focus on the
110        // undeleted item.
111        //
112        // 2. Otherwise try to focus on the MediaItem that is currently focused,
113        // if we can find it.
114        //
115        // 3. Otherwise try to focus on the previous MediaItem or the next
116        // MediaItem, depending on the value of focus hint direction.
117        public static final int FOCUS_HINT_NEXT = 0;
118        public static final int FOCUS_HINT_PREVIOUS = 1;
119        public void setFocusHintDirection(int direction);
120        public void setFocusHintPath(Path path);
121    }
122
123    public interface Listener {
124        public void onSingleTapUp(int x, int y);
125        public void onFullScreenChanged(boolean full);
126        public void onActionBarAllowed(boolean allowed);
127        public void onActionBarWanted();
128        public void onCurrentImageUpdated();
129        public void onDeleteImage(Path path, int offset);
130        public void onUndoDeleteImage();
131        public void onCommitDeleteImage();
132        public void onFilmModeChanged(boolean enabled);
133        public void onPictureCenter(boolean isCamera);
134        public void onUndoBarVisibilityChanged(boolean visible);
135    }
136
137    // The rules about orientation locking:
138    //
139    // (1) We need to lock the orientation if we are in page mode camera
140    // preview, so there is no (unwanted) rotation animation when the user
141    // rotates the device.
142    //
143    // (2) We need to unlock the orientation if we want to show the action bar
144    // because the action bar follows the system orientation.
145    //
146    // The rules about action bar:
147    //
148    // (1) If we are in film mode, we don't show action bar.
149    //
150    // (2) If we go from camera to gallery with capture animation, we show
151    // action bar.
152    private static final int MSG_CANCEL_EXTRA_SCALING = 2;
153    private static final int MSG_SWITCH_FOCUS = 3;
154    private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
155    private static final int MSG_DELETE_ANIMATION_DONE = 5;
156    private static final int MSG_DELETE_DONE = 6;
157    private static final int MSG_UNDO_BAR_TIMEOUT = 7;
158    private static final int MSG_UNDO_BAR_FULL_CAMERA = 8;
159
160    private static final float SWIPE_THRESHOLD = 300f;
161
162    private static final float DEFAULT_TEXT_SIZE = 20;
163    private static float TRANSITION_SCALE_FACTOR = 0.74f;
164    private static final int ICON_RATIO = 6;
165
166    // whether we want to apply card deck effect in page mode.
167    private static final boolean CARD_EFFECT = true;
168
169    // whether we want to apply offset effect in film mode.
170    private static final boolean OFFSET_EFFECT = true;
171
172    // Used to calculate the scaling factor for the card deck effect.
173    private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
174
175    // Used to calculate the alpha factor for the fading animation.
176    private AccelerateInterpolator mAlphaInterpolator =
177            new AccelerateInterpolator(0.9f);
178
179    // We keep this many previous ScreenNails. (also this many next ScreenNails)
180    public static final int SCREEN_NAIL_MAX = 3;
181
182    // These are constants for the delete gesture.
183    private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
184    private static final int MAX_DISMISS_VELOCITY = 2500; // dp/sec
185    private static final int SWIPE_ESCAPE_DISTANCE = 150; // dp
186
187    // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
188    // SCREEN_NAIL_MAX.
189    private final RangeArray<Picture> mPictures =
190            new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
191    private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
192
193    private final MyGestureListener mGestureListener;
194    private final GestureRecognizer mGestureRecognizer;
195    private final PositionController mPositionController;
196
197    private Listener mListener;
198    private Model mModel;
199    private StringTexture mNoThumbnailText;
200    private TileImageView mTileView;
201    private EdgeView mEdgeView;
202    private UndoBarView mUndoBar;
203    private Texture mVideoPlayIcon;
204
205    private SynchronizedHandler mHandler;
206
207    private boolean mCancelExtraScalingPending;
208    private boolean mFilmMode = false;
209    private boolean mWantPictureCenterCallbacks = false;
210    private int mDisplayRotation = 0;
211    private int mCompensation = 0;
212    private boolean mFullScreenCamera;
213    private Rect mCameraRelativeFrame = new Rect();
214    private Rect mCameraRect = new Rect();
215    private boolean mFirst = true;
216
217    // [mPrevBound, mNextBound] is the range of index for all pictures in the
218    // model, if we assume the index of current focused picture is 0.  So if
219    // there are some previous pictures, mPrevBound < 0, and if there are some
220    // next pictures, mNextBound > 0.
221    private int mPrevBound;
222    private int mNextBound;
223
224    // This variable prevents us doing snapback until its values goes to 0. This
225    // happens if the user gesture is still in progress or we are in a capture
226    // animation.
227    private int mHolding;
228    private static final int HOLD_TOUCH_DOWN = 1;
229    private static final int HOLD_CAPTURE_ANIMATION = 2;
230    private static final int HOLD_DELETE = 4;
231
232    // mTouchBoxIndex is the index of the box that is touched by the down
233    // gesture in film mode. The value Integer.MAX_VALUE means no box was
234    // touched.
235    private int mTouchBoxIndex = Integer.MAX_VALUE;
236    // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
237    // if mTouchBoxIndex is not Integer.MAX_VALUE.
238    private boolean mTouchBoxDeletable;
239    // This is the index of the last deleted item. This is only used as a hint
240    // to hide the undo button when we are too far away from the deleted
241    // item. The value Integer.MAX_VALUE means there is no such hint.
242    private int mUndoIndexHint = Integer.MAX_VALUE;
243
244    private Context mContext;
245
246    public PhotoView(AbstractGalleryActivity activity) {
247        mTileView = new TileImageView(activity);
248        addComponent(mTileView);
249        mContext = activity.getAndroidContext();
250        mPlaceholderColor = mContext.getResources().getColor(
251                R.color.photo_placeholder);
252        mEdgeView = new EdgeView(mContext);
253        addComponent(mEdgeView);
254        mUndoBar = new UndoBarView(mContext);
255        addComponent(mUndoBar);
256        mUndoBar.setVisibility(GLView.INVISIBLE);
257        mUndoBar.setOnClickListener(new OnClickListener() {
258                @Override
259                public void onClick(GLView v) {
260                    mListener.onUndoDeleteImage();
261                    hideUndoBar();
262                }
263            });
264        mNoThumbnailText = StringTexture.newInstance(
265                mContext.getString(R.string.no_thumbnail),
266                DEFAULT_TEXT_SIZE, Color.WHITE);
267
268        mHandler = new MyHandler(activity.getGLRoot());
269
270        mGestureListener = new MyGestureListener();
271        mGestureRecognizer = new GestureRecognizer(mContext, mGestureListener);
272
273        mPositionController = new PositionController(mContext,
274                new PositionController.Listener() {
275
276            @Override
277            public void invalidate() {
278                PhotoView.this.invalidate();
279            }
280
281            @Override
282            public boolean isHoldingDown() {
283                return (mHolding & HOLD_TOUCH_DOWN) != 0;
284            }
285
286            @Override
287            public boolean isHoldingDelete() {
288                return (mHolding & HOLD_DELETE) != 0;
289            }
290
291            @Override
292            public void onPull(int offset, int direction) {
293                mEdgeView.onPull(offset, direction);
294            }
295
296            @Override
297            public void onRelease() {
298                mEdgeView.onRelease();
299            }
300
301            @Override
302            public void onAbsorb(int velocity, int direction) {
303                mEdgeView.onAbsorb(velocity, direction);
304            }
305        });
306        mVideoPlayIcon = new ResourceTexture(mContext, R.drawable.ic_control_play);
307        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
308            if (i == 0) {
309                mPictures.put(i, new FullPicture());
310            } else {
311                mPictures.put(i, new ScreenNailPicture(i));
312            }
313        }
314    }
315
316    public void stopScrolling() {
317        mPositionController.stopScrolling();
318    }
319
320    public void setModel(Model model) {
321        mModel = model;
322        mTileView.setModel(mModel);
323    }
324
325    class MyHandler extends SynchronizedHandler {
326        public MyHandler(GLRoot root) {
327            super(root);
328        }
329
330        @Override
331        public void handleMessage(Message message) {
332            switch (message.what) {
333                case MSG_CANCEL_EXTRA_SCALING: {
334                    mGestureRecognizer.cancelScale();
335                    mPositionController.setExtraScalingRange(false);
336                    mCancelExtraScalingPending = false;
337                    break;
338                }
339                case MSG_SWITCH_FOCUS: {
340                    switchFocus();
341                    break;
342                }
343                case MSG_CAPTURE_ANIMATION_DONE: {
344                    // message.arg1 is the offset parameter passed to
345                    // switchWithCaptureAnimation().
346                    captureAnimationDone(message.arg1);
347                    break;
348                }
349                case MSG_DELETE_ANIMATION_DONE: {
350                    // message.obj is the Path of the MediaItem which should be
351                    // deleted. message.arg1 is the offset of the image.
352                    mListener.onDeleteImage((Path) message.obj, message.arg1);
353                    // Normally a box which finishes delete animation will hold
354                    // position until the underlying MediaItem is actually
355                    // deleted, and HOLD_DELETE will be cancelled that time. In
356                    // case the MediaItem didn't actually get deleted in 2
357                    // seconds, we will cancel HOLD_DELETE and make it bounce
358                    // back.
359
360                    // We make sure there is at most one MSG_DELETE_DONE
361                    // in the handler.
362                    mHandler.removeMessages(MSG_DELETE_DONE);
363                    Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
364                    mHandler.sendMessageDelayed(m, 2000);
365
366                    int numberOfPictures = mNextBound - mPrevBound + 1;
367                    if (numberOfPictures == 2) {
368                        if (mModel.isCamera(mNextBound)
369                                || mModel.isCamera(mPrevBound)) {
370                            numberOfPictures--;
371                        }
372                    }
373                    showUndoBar(numberOfPictures <= 1);
374                    break;
375                }
376                case MSG_DELETE_DONE: {
377                    if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
378                        mHolding &= ~HOLD_DELETE;
379                        snapback();
380                    }
381                    break;
382                }
383                case MSG_UNDO_BAR_TIMEOUT: {
384                    checkHideUndoBar(UNDO_BAR_TIMEOUT);
385                    break;
386                }
387                case MSG_UNDO_BAR_FULL_CAMERA: {
388                    checkHideUndoBar(UNDO_BAR_FULL_CAMERA);
389                    break;
390                }
391                default: throw new AssertionError(message.what);
392            }
393        }
394    }
395
396    public void setWantPictureCenterCallbacks(boolean wanted) {
397        mWantPictureCenterCallbacks = wanted;
398    }
399
400    ////////////////////////////////////////////////////////////////////////////
401    //  Data/Image change notifications
402    ////////////////////////////////////////////////////////////////////////////
403
404    public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
405        mPrevBound = prevBound;
406        mNextBound = nextBound;
407
408        // Update mTouchBoxIndex
409        if (mTouchBoxIndex != Integer.MAX_VALUE) {
410            int k = mTouchBoxIndex;
411            mTouchBoxIndex = Integer.MAX_VALUE;
412            for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
413                if (fromIndex[i] == k) {
414                    mTouchBoxIndex = i - SCREEN_NAIL_MAX;
415                    break;
416                }
417            }
418        }
419
420        // Hide undo button if we are too far away
421        if (mUndoIndexHint != Integer.MAX_VALUE) {
422            if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
423                hideUndoBar();
424            }
425        }
426
427        // Update the ScreenNails.
428        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
429            Picture p =  mPictures.get(i);
430            p.reload();
431            mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
432        }
433
434        boolean wasDeleting = mPositionController.hasDeletingBox();
435
436        // Move the boxes
437        mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
438                mModel.isCamera(0), mSizes);
439
440        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
441            setPictureSize(i);
442        }
443
444        boolean isDeleting = mPositionController.hasDeletingBox();
445
446        // If the deletion is done, make HOLD_DELETE persist for only the time
447        // needed for a snapback animation.
448        if (wasDeleting && !isDeleting) {
449            mHandler.removeMessages(MSG_DELETE_DONE);
450            Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
451            mHandler.sendMessageDelayed(
452                    m, PositionController.SNAPBACK_ANIMATION_TIME);
453        }
454
455        invalidate();
456    }
457
458    public boolean isDeleting() {
459        return (mHolding & HOLD_DELETE) != 0
460                && mPositionController.hasDeletingBox();
461    }
462
463    public void notifyImageChange(int index) {
464        if (index == 0) {
465            mListener.onCurrentImageUpdated();
466        }
467        mPictures.get(index).reload();
468        setPictureSize(index);
469        invalidate();
470    }
471
472    private void setPictureSize(int index) {
473        Picture p = mPictures.get(index);
474        mPositionController.setImageSize(index, p.getSize(),
475                index == 0 && p.isCamera() ? mCameraRect : null);
476    }
477
478    @Override
479    protected void onLayout(
480            boolean changeSize, int left, int top, int right, int bottom) {
481        int w = right - left;
482        int h = bottom - top;
483        mTileView.layout(0, 0, w, h);
484        mEdgeView.layout(0, 0, w, h);
485        mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
486        mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
487
488        GLRoot root = getGLRoot();
489        int displayRotation = root.getDisplayRotation();
490        int compensation = root.getCompensation();
491        if (mDisplayRotation != displayRotation
492                || mCompensation != compensation) {
493            mDisplayRotation = displayRotation;
494            mCompensation = compensation;
495
496            // We need to change the size and rotation of the Camera ScreenNail,
497            // but we don't want it to animate because the size doen't actually
498            // change in the eye of the user.
499            for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
500                Picture p = mPictures.get(i);
501                if (p.isCamera()) {
502                    p.forceSize();
503                }
504            }
505        }
506
507        updateCameraRect();
508        mPositionController.setConstrainedFrame(mCameraRect);
509        if (changeSize) {
510            mPositionController.setViewSize(getWidth(), getHeight());
511        }
512    }
513
514    // Update the camera rectangle due to layout change or camera relative frame
515    // change.
516    private void updateCameraRect() {
517        // Get the width and height in framework orientation because the given
518        // mCameraRelativeFrame is in that coordinates.
519        int w = getWidth();
520        int h = getHeight();
521        if (mCompensation % 180 != 0) {
522            int tmp = w;
523            w = h;
524            h = tmp;
525        }
526        int l = mCameraRelativeFrame.left;
527        int t = mCameraRelativeFrame.top;
528        int r = mCameraRelativeFrame.right;
529        int b = mCameraRelativeFrame.bottom;
530
531        // Now convert it to the coordinates we are using.
532        switch (mCompensation) {
533            case 0: mCameraRect.set(l, t, r, b); break;
534            case 90: mCameraRect.set(h - b, l, h - t, r); break;
535            case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
536            case 270: mCameraRect.set(t, w - r, b, w - l); break;
537        }
538
539        Log.d(TAG, "compensation = " + mCompensation
540                + ", CameraRelativeFrame = " + mCameraRelativeFrame
541                + ", mCameraRect = " + mCameraRect);
542    }
543
544    public void setCameraRelativeFrame(Rect frame) {
545        mCameraRelativeFrame.set(frame);
546        updateCameraRect();
547        // Originally we do
548        //     mPositionController.setConstrainedFrame(mCameraRect);
549        // here, but it is moved to a parameter of the setImageSize() call, so
550        // it can be updated atomically with the CameraScreenNail's size change.
551    }
552
553    // Returns the rotation we need to do to the camera texture before drawing
554    // it to the canvas, assuming the camera texture is correct when the device
555    // is in its natural orientation.
556    private int getCameraRotation() {
557        return (mCompensation - mDisplayRotation + 360) % 360;
558    }
559
560    private int getPanoramaRotation() {
561        // This function is magic
562        // The issue here is that Pano makes bad assumptions about rotation and
563        // orientation. The first is it assumes only two rotations are possible,
564        // 0 and 90. Thus, if display rotation is >= 180, we invert the output.
565        // The second is that it assumes landscape is a 90 rotation from portrait,
566        // however on landscape devices this is not true. Thus, if we are in portrait
567        // on a landscape device, we need to invert the output
568        int orientation = mContext.getResources().getConfiguration().orientation;
569        boolean invertPortrait = (orientation == Configuration.ORIENTATION_PORTRAIT
570                && (mDisplayRotation == 90 || mDisplayRotation == 270));
571        boolean invert = (mDisplayRotation >= 180);
572        if (invert != invertPortrait) {
573            return (mCompensation + 180) % 360;
574        }
575        return mCompensation;
576    }
577
578    ////////////////////////////////////////////////////////////////////////////
579    //  Pictures
580    ////////////////////////////////////////////////////////////////////////////
581
582    private interface Picture {
583        void reload();
584        void draw(GLCanvas canvas, Rect r);
585        void setScreenNail(ScreenNail s);
586        boolean isCamera();  // whether the picture is a camera preview
587        boolean isDeletable();  // whether the picture can be deleted
588        void forceSize();  // called when mCompensation changes
589        Size getSize();
590    }
591
592    class FullPicture implements Picture {
593        private int mRotation;
594        private boolean mIsCamera;
595        private boolean mIsPanorama;
596        private boolean mIsStaticCamera;
597        private boolean mIsVideo;
598        private boolean mIsDeletable;
599        private int mLoadingState = Model.LOADING_INIT;
600        private Size mSize = new Size();
601
602        @Override
603        public void reload() {
604            // mImageWidth and mImageHeight will get updated
605            mTileView.notifyModelInvalidated();
606
607            mIsCamera = mModel.isCamera(0);
608            mIsPanorama = mModel.isPanorama(0);
609            mIsStaticCamera = mModel.isStaticCamera(0);
610            mIsVideo = mModel.isVideo(0);
611            mIsDeletable = mModel.isDeletable(0);
612            mLoadingState = mModel.getLoadingState(0);
613            setScreenNail(mModel.getScreenNail(0));
614            updateSize();
615        }
616
617        @Override
618        public Size getSize() {
619            return mSize;
620        }
621
622        @Override
623        public void forceSize() {
624            updateSize();
625            mPositionController.forceImageSize(0, mSize);
626        }
627
628        private void updateSize() {
629            if (mIsPanorama) {
630                mRotation = getPanoramaRotation();
631            } else if (mIsCamera && !mIsStaticCamera) {
632                mRotation = getCameraRotation();
633            } else {
634                mRotation = mModel.getImageRotation(0);
635            }
636
637            int w = mTileView.mImageWidth;
638            int h = mTileView.mImageHeight;
639            mSize.width = getRotated(mRotation, w, h);
640            mSize.height = getRotated(mRotation, h, w);
641        }
642
643        @Override
644        public void draw(GLCanvas canvas, Rect r) {
645            drawTileView(canvas, r);
646
647            // We want to have the following transitions:
648            // (1) Move camera preview out of its place: switch to film mode
649            // (2) Move camera preview into its place: switch to page mode
650            // The extra mWasCenter check makes sure (1) does not apply if in
651            // page mode, we move _to_ the camera preview from another picture.
652
653            // Holdings except touch-down prevent the transitions.
654            if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
655
656            if (mWantPictureCenterCallbacks && mPositionController.isCenter()) {
657                mListener.onPictureCenter(mIsCamera);
658            }
659        }
660
661        @Override
662        public void setScreenNail(ScreenNail s) {
663            mTileView.setScreenNail(s);
664        }
665
666        @Override
667        public boolean isCamera() {
668            return mIsCamera;
669        }
670
671        @Override
672        public boolean isDeletable() {
673            return mIsDeletable;
674        }
675
676        private void drawTileView(GLCanvas canvas, Rect r) {
677            float imageScale = mPositionController.getImageScale();
678            int viewW = getWidth();
679            int viewH = getHeight();
680            float cx = r.exactCenterX();
681            float cy = r.exactCenterY();
682            float scale = 1f;  // the scaling factor due to card effect
683
684            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
685            float filmRatio = mPositionController.getFilmRatio();
686            boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
687                    && filmRatio != 1f && !mPictures.get(-1).isCamera()
688                    && !mPositionController.inOpeningAnimation();
689            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
690                    && filmRatio == 1f && r.centerY() != viewH / 2;
691            if (wantsCardEffect) {
692                // Calculate the move-out progress value.
693                int left = r.left;
694                int right = r.right;
695                float progress = calculateMoveOutProgress(left, right, viewW);
696                progress = Utils.clamp(progress, -1f, 1f);
697
698                // We only want to apply the fading animation if the scrolling
699                // movement is to the right.
700                if (progress < 0) {
701                    scale = getScrollScale(progress);
702                    float alpha = getScrollAlpha(progress);
703                    scale = interpolate(filmRatio, scale, 1f);
704                    alpha = interpolate(filmRatio, alpha, 1f);
705
706                    imageScale *= scale;
707                    canvas.multiplyAlpha(alpha);
708
709                    float cxPage;  // the cx value in page mode
710                    if (right - left <= viewW) {
711                        // If the picture is narrower than the view, keep it at
712                        // the center of the view.
713                        cxPage = viewW / 2f;
714                    } else {
715                        // If the picture is wider than the view (it's
716                        // zoomed-in), keep the left edge of the object align
717                        // the the left edge of the view.
718                        cxPage = (right - left) * scale / 2f;
719                    }
720                    cx = interpolate(filmRatio, cxPage, cx);
721                }
722            } else if (wantsOffsetEffect) {
723                float offset = (float) (r.centerY() - viewH / 2) / viewH;
724                float alpha = getOffsetAlpha(offset);
725                canvas.multiplyAlpha(alpha);
726            }
727
728            // Draw the tile view.
729            setTileViewPosition(cx, cy, viewW, viewH, imageScale);
730            renderChild(canvas, mTileView);
731
732            // Draw the play video icon and the message.
733            canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
734            int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
735            if (mIsVideo) drawVideoPlayIcon(canvas, s);
736            if (mLoadingState == Model.LOADING_FAIL) {
737                drawLoadingFailMessage(canvas);
738            }
739
740            // Draw a debug indicator showing which picture has focus (index ==
741            // 0).
742            //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
743
744            canvas.restore();
745        }
746
747        // Set the position of the tile view
748        private void setTileViewPosition(float cx, float cy,
749                int viewW, int viewH, float scale) {
750            // Find out the bitmap coordinates of the center of the view
751            int imageW = mPositionController.getImageWidth();
752            int imageH = mPositionController.getImageHeight();
753            int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
754            int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
755
756            int inverseX = imageW - centerX;
757            int inverseY = imageH - centerY;
758            int x, y;
759            switch (mRotation) {
760                case 0: x = centerX; y = centerY; break;
761                case 90: x = centerY; y = inverseX; break;
762                case 180: x = inverseX; y = inverseY; break;
763                case 270: x = inverseY; y = centerX; break;
764                default:
765                    throw new RuntimeException(String.valueOf(mRotation));
766            }
767            mTileView.setPosition(x, y, scale, mRotation);
768        }
769    }
770
771    private class ScreenNailPicture implements Picture {
772        private int mIndex;
773        private int mRotation;
774        private ScreenNail mScreenNail;
775        private boolean mIsCamera;
776        private boolean mIsPanorama;
777        private boolean mIsStaticCamera;
778        private boolean mIsVideo;
779        private boolean mIsDeletable;
780        private int mLoadingState = Model.LOADING_INIT;
781        private Size mSize = new Size();
782
783        public ScreenNailPicture(int index) {
784            mIndex = index;
785        }
786
787        @Override
788        public void reload() {
789            mIsCamera = mModel.isCamera(mIndex);
790            mIsPanorama = mModel.isPanorama(mIndex);
791            mIsStaticCamera = mModel.isStaticCamera(mIndex);
792            mIsVideo = mModel.isVideo(mIndex);
793            mIsDeletable = mModel.isDeletable(mIndex);
794            mLoadingState = mModel.getLoadingState(mIndex);
795            setScreenNail(mModel.getScreenNail(mIndex));
796            updateSize();
797        }
798
799        @Override
800        public Size getSize() {
801            return mSize;
802        }
803
804        @Override
805        public void draw(GLCanvas canvas, Rect r) {
806            if (mScreenNail == null) {
807                // Draw a placeholder rectange if there should be a picture in
808                // this position (but somehow there isn't).
809                if (mIndex >= mPrevBound && mIndex <= mNextBound) {
810                    drawPlaceHolder(canvas, r);
811                }
812                return;
813            }
814            int w = getWidth();
815            int h = getHeight();
816            if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
817                mScreenNail.noDraw();
818                return;
819            }
820
821            float filmRatio = mPositionController.getFilmRatio();
822            boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
823                    && filmRatio != 1f && !mPictures.get(0).isCamera();
824            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
825                    && filmRatio == 1f && r.centerY() != h / 2;
826            int cx = wantsCardEffect
827                    ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
828                    : r.centerX();
829            int cy = r.centerY();
830            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
831            canvas.translate(cx, cy);
832            if (wantsCardEffect) {
833                float progress = (float) (w / 2 - r.centerX()) / w;
834                progress = Utils.clamp(progress, -1, 1);
835                float alpha = getScrollAlpha(progress);
836                float scale = getScrollScale(progress);
837                alpha = interpolate(filmRatio, alpha, 1f);
838                scale = interpolate(filmRatio, scale, 1f);
839                canvas.multiplyAlpha(alpha);
840                canvas.scale(scale, scale, 1);
841            } else if (wantsOffsetEffect) {
842                float offset = (float) (r.centerY() - h / 2) / h;
843                float alpha = getOffsetAlpha(offset);
844                canvas.multiplyAlpha(alpha);
845            }
846            if (mRotation != 0) {
847                canvas.rotate(mRotation, 0, 0, 1);
848            }
849            int drawW = getRotated(mRotation, r.width(), r.height());
850            int drawH = getRotated(mRotation, r.height(), r.width());
851            mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
852            if (isScreenNailAnimating()) {
853                invalidate();
854            }
855            int s = Math.min(drawW, drawH);
856            if (mIsVideo) drawVideoPlayIcon(canvas, s);
857            if (mLoadingState == Model.LOADING_FAIL) {
858                drawLoadingFailMessage(canvas);
859            }
860            canvas.restore();
861        }
862
863        private boolean isScreenNailAnimating() {
864            return (mScreenNail instanceof TiledScreenNail)
865                    && ((TiledScreenNail) mScreenNail).isAnimating();
866        }
867
868        @Override
869        public void setScreenNail(ScreenNail s) {
870            mScreenNail = s;
871        }
872
873        @Override
874        public void forceSize() {
875            updateSize();
876            mPositionController.forceImageSize(mIndex, mSize);
877        }
878
879        private void updateSize() {
880            if (mIsPanorama) {
881                mRotation = getPanoramaRotation();
882            } else if (mIsCamera && !mIsStaticCamera) {
883                mRotation = getCameraRotation();
884            } else {
885                mRotation = mModel.getImageRotation(mIndex);
886            }
887
888            if (mScreenNail != null) {
889                mSize.width = mScreenNail.getWidth();
890                mSize.height = mScreenNail.getHeight();
891            } else {
892                // If we don't have ScreenNail available, we can still try to
893                // get the size information of it.
894                mModel.getImageSize(mIndex, mSize);
895            }
896
897            int w = mSize.width;
898            int h = mSize.height;
899            mSize.width = getRotated(mRotation, w, h);
900            mSize.height = getRotated(mRotation, h, w);
901        }
902
903        @Override
904        public boolean isCamera() {
905            return mIsCamera;
906        }
907
908        @Override
909        public boolean isDeletable() {
910            return mIsDeletable;
911        }
912    }
913
914    // Draw a gray placeholder in the specified rectangle.
915    private void drawPlaceHolder(GLCanvas canvas, Rect r) {
916        canvas.fillRect(r.left, r.top, r.width(), r.height(), mPlaceholderColor);
917    }
918
919    // Draw the video play icon (in the place where the spinner was)
920    private void drawVideoPlayIcon(GLCanvas canvas, int side) {
921        int s = side / ICON_RATIO;
922        // Draw the video play icon at the center
923        mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
924    }
925
926    // Draw the "no thumbnail" message
927    private void drawLoadingFailMessage(GLCanvas canvas) {
928        StringTexture m = mNoThumbnailText;
929        m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
930    }
931
932    private static int getRotated(int degree, int original, int theother) {
933        return (degree % 180 == 0) ? original : theother;
934    }
935
936    ////////////////////////////////////////////////////////////////////////////
937    //  Gestures Handling
938    ////////////////////////////////////////////////////////////////////////////
939
940    @Override
941    protected boolean onTouch(MotionEvent event) {
942        mGestureRecognizer.onTouchEvent(event);
943        return true;
944    }
945
946    private class MyGestureListener implements GestureRecognizer.Listener {
947        private boolean mIgnoreUpEvent = false;
948        // If we can change mode for this scale gesture.
949        private boolean mCanChangeMode;
950        // If we have changed the film mode in this scaling gesture.
951        private boolean mModeChanged;
952        // If this scaling gesture should be ignored.
953        private boolean mIgnoreScalingGesture;
954        // whether the down action happened while the view is scrolling.
955        private boolean mDownInScrolling;
956        // If we should ignore all gestures other than onSingleTapUp.
957        private boolean mIgnoreSwipingGesture;
958        // If a scrolling has happened after a down gesture.
959        private boolean mScrolledAfterDown;
960        // If the first scrolling move is in X direction. In the film mode, X
961        // direction scrolling is normal scrolling. but Y direction scrolling is
962        // a delete gesture.
963        private boolean mFirstScrollX;
964        // The accumulated Y delta that has been sent to mPositionController.
965        private int mDeltaY;
966        // The accumulated scaling change from a scaling gesture.
967        private float mAccScale;
968        // If an onFling happened after the last onDown
969        private boolean mHadFling;
970
971        @Override
972        public boolean onSingleTapUp(float x, float y) {
973            // On crespo running Android 2.3.6 (gingerbread), a pinch out gesture results in the
974            // following call sequence: onDown(), onUp() and then onSingleTapUp(). The correct
975            // sequence for a single-tap-up gesture should be: onDown(), onSingleTapUp() and onUp().
976            // The call sequence for a pinch out gesture in JB is: onDown(), then onUp() and there's
977            // no onSingleTapUp(). Base on these observations, the following condition is added to
978            // filter out the false alarm where onSingleTapUp() is called within a pinch out
979            // gesture. The framework fix went into ICS. Refer to b/4588114.
980            if (Build.VERSION.SDK_INT < ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) {
981                if ((mHolding & HOLD_TOUCH_DOWN) == 0) {
982                    return true;
983                }
984            }
985
986            // We do this in addition to onUp() because we want the snapback of
987            // setFilmMode to happen.
988            mHolding &= ~HOLD_TOUCH_DOWN;
989
990            if (mFilmMode && !mDownInScrolling) {
991                switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
992
993                // If this is a lock screen photo, let the listener handle the
994                // event. Tapping on lock screen photo should take the user
995                // directly to the lock screen.
996                MediaItem item = mModel.getMediaItem(0);
997                int supported = 0;
998                if (item != null) supported = item.getSupportedOperations();
999                if ((supported & MediaItem.SUPPORT_ACTION) == 0) {
1000                    setFilmMode(false);
1001                    mIgnoreUpEvent = true;
1002                    return true;
1003                }
1004            }
1005
1006            if (mListener != null) {
1007                // Do the inverse transform of the touch coordinates.
1008                Matrix m = getGLRoot().getCompensationMatrix();
1009                Matrix inv = new Matrix();
1010                m.invert(inv);
1011                float[] pts = new float[] {x, y};
1012                inv.mapPoints(pts);
1013                mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
1014            }
1015            return true;
1016        }
1017
1018        @Override
1019        public boolean onDoubleTap(float x, float y) {
1020            if (mIgnoreSwipingGesture) return true;
1021            if (mPictures.get(0).isCamera()) return false;
1022            PositionController controller = mPositionController;
1023            float scale = controller.getImageScale();
1024            // onDoubleTap happened on the second ACTION_DOWN.
1025            // We need to ignore the next UP event.
1026            mIgnoreUpEvent = true;
1027            if (scale <= .75f || controller.isAtMinimalScale()) {
1028                controller.zoomIn(x, y, Math.max(1.0f, scale * 1.5f));
1029            } else {
1030                controller.resetToFullView();
1031            }
1032            return true;
1033        }
1034
1035        @Override
1036        public boolean onScroll(float dx, float dy, float totalX, float totalY) {
1037            if (mIgnoreSwipingGesture) return true;
1038            if (!mScrolledAfterDown) {
1039                mScrolledAfterDown = true;
1040                mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
1041            }
1042
1043            int dxi = (int) (-dx + 0.5f);
1044            int dyi = (int) (-dy + 0.5f);
1045            if (mFilmMode) {
1046                if (mFirstScrollX) {
1047                    mPositionController.scrollFilmX(dxi);
1048                } else {
1049                    if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
1050                    int newDeltaY = calculateDeltaY(totalY);
1051                    int d = newDeltaY - mDeltaY;
1052                    if (d != 0) {
1053                        mPositionController.scrollFilmY(mTouchBoxIndex, d);
1054                        mDeltaY = newDeltaY;
1055                    }
1056                }
1057            } else {
1058                mPositionController.scrollPage(dxi, dyi);
1059            }
1060            return true;
1061        }
1062
1063        private int calculateDeltaY(float delta) {
1064            if (mTouchBoxDeletable) return (int) (delta + 0.5f);
1065
1066            // don't let items that can't be deleted be dragged more than
1067            // maxScrollDistance, and make it harder and harder to drag.
1068            int size = getHeight();
1069            float maxScrollDistance = 0.15f * size;
1070            if (Math.abs(delta) >= size) {
1071                delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
1072            } else {
1073                delta = maxScrollDistance *
1074                        FloatMath.sin((delta / size) * (float) (Math.PI / 2));
1075            }
1076            return (int) (delta + 0.5f);
1077        }
1078
1079        @Override
1080        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
1081            if (mIgnoreSwipingGesture) return true;
1082            if (mModeChanged) return true;
1083            if (swipeImages(velocityX, velocityY)) {
1084                mIgnoreUpEvent = true;
1085            } else {
1086                flingImages(velocityX, velocityY, Math.abs(e2.getY() - e1.getY()));
1087            }
1088            mHadFling = true;
1089            return true;
1090        }
1091
1092        private boolean flingImages(float velocityX, float velocityY, float dY) {
1093            int vx = (int) (velocityX + 0.5f);
1094            int vy = (int) (velocityY + 0.5f);
1095            if (!mFilmMode) {
1096                return mPositionController.flingPage(vx, vy);
1097            }
1098            if (Math.abs(velocityX) > Math.abs(velocityY)) {
1099                return mPositionController.flingFilmX(vx);
1100            }
1101            // If we scrolled in Y direction fast enough, treat it as a delete
1102            // gesture.
1103            if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
1104                    || !mTouchBoxDeletable) {
1105                return false;
1106            }
1107            int maxVelocity = GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
1108            int escapeVelocity = GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
1109            int escapeDistance = GalleryUtils.dpToPixel(SWIPE_ESCAPE_DISTANCE);
1110            int centerY = mPositionController.getPosition(mTouchBoxIndex)
1111                    .centerY();
1112            boolean fastEnough = (Math.abs(vy) > escapeVelocity)
1113                    && (Math.abs(vy) > Math.abs(vx))
1114                    && ((vy > 0) == (centerY > getHeight() / 2))
1115                    && dY >= escapeDistance;
1116            if (fastEnough) {
1117                vy = Math.min(vy, maxVelocity);
1118                int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
1119                if (duration >= 0) {
1120                    mPositionController.setPopFromTop(vy < 0);
1121                    deleteAfterAnimation(duration);
1122                    // We reset mTouchBoxIndex, so up() won't check if Y
1123                    // scrolled far enough to be a delete gesture.
1124                    mTouchBoxIndex = Integer.MAX_VALUE;
1125                    return true;
1126                }
1127            }
1128            return false;
1129        }
1130
1131        private void deleteAfterAnimation(int duration) {
1132            MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
1133            if (item == null) return;
1134            mListener.onCommitDeleteImage();
1135            mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
1136            mHolding |= HOLD_DELETE;
1137            Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
1138            m.obj = item.getPath();
1139            m.arg1 = mTouchBoxIndex;
1140            mHandler.sendMessageDelayed(m, duration);
1141        }
1142
1143        @Override
1144        public boolean onScaleBegin(float focusX, float focusY) {
1145            if (mIgnoreSwipingGesture) return true;
1146            // We ignore the scaling gesture if it is a camera preview.
1147            mIgnoreScalingGesture = mPictures.get(0).isCamera();
1148            if (mIgnoreScalingGesture) {
1149                return true;
1150            }
1151            mPositionController.beginScale(focusX, focusY);
1152            // We can change mode if we are in film mode, or we are in page
1153            // mode and at minimal scale.
1154            mCanChangeMode = mFilmMode
1155                    || mPositionController.isAtMinimalScale();
1156            mAccScale = 1f;
1157            return true;
1158        }
1159
1160        @Override
1161        public boolean onScale(float focusX, float focusY, float scale) {
1162            if (mIgnoreSwipingGesture) return true;
1163            if (mIgnoreScalingGesture) return true;
1164            if (mModeChanged) return true;
1165            if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
1166
1167            int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
1168
1169            // We wait for a large enough scale change before changing mode.
1170            // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
1171            // or vice versa.
1172            mAccScale *= scale;
1173            boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
1174
1175            // If mode changes, we treat this scaling gesture has ended.
1176            if (mCanChangeMode && largeEnough) {
1177                if ((outOfRange < 0 && !mFilmMode) ||
1178                        (outOfRange > 0 && mFilmMode)) {
1179                    stopExtraScalingIfNeeded();
1180
1181                    // Removing the touch down flag allows snapback to happen
1182                    // for film mode change.
1183                    mHolding &= ~HOLD_TOUCH_DOWN;
1184                    if (mFilmMode) {
1185                        UsageStatistics.setPendingTransitionCause(
1186                                UsageStatistics.TRANSITION_PINCH_OUT);
1187                    } else {
1188                        UsageStatistics.setPendingTransitionCause(
1189                                UsageStatistics.TRANSITION_PINCH_IN);
1190                    }
1191                    setFilmMode(!mFilmMode);
1192
1193
1194                    // We need to call onScaleEnd() before setting mModeChanged
1195                    // to true.
1196                    onScaleEnd();
1197                    mModeChanged = true;
1198                    return true;
1199                }
1200           }
1201
1202            if (outOfRange != 0) {
1203                startExtraScalingIfNeeded();
1204            } else {
1205                stopExtraScalingIfNeeded();
1206            }
1207            return true;
1208        }
1209
1210        @Override
1211        public void onScaleEnd() {
1212            if (mIgnoreSwipingGesture) return;
1213            if (mIgnoreScalingGesture) return;
1214            if (mModeChanged) return;
1215            mPositionController.endScale();
1216        }
1217
1218        private void startExtraScalingIfNeeded() {
1219            if (!mCancelExtraScalingPending) {
1220                mHandler.sendEmptyMessageDelayed(
1221                        MSG_CANCEL_EXTRA_SCALING, 700);
1222                mPositionController.setExtraScalingRange(true);
1223                mCancelExtraScalingPending = true;
1224            }
1225        }
1226
1227        private void stopExtraScalingIfNeeded() {
1228            if (mCancelExtraScalingPending) {
1229                mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
1230                mPositionController.setExtraScalingRange(false);
1231                mCancelExtraScalingPending = false;
1232            }
1233        }
1234
1235        @Override
1236        public void onDown(float x, float y) {
1237            checkHideUndoBar(UNDO_BAR_TOUCHED);
1238
1239            mDeltaY = 0;
1240            mModeChanged = false;
1241
1242            if (mIgnoreSwipingGesture) return;
1243
1244            mHolding |= HOLD_TOUCH_DOWN;
1245
1246            if (mFilmMode && mPositionController.isScrolling()) {
1247                mDownInScrolling = true;
1248                mPositionController.stopScrolling();
1249            } else {
1250                mDownInScrolling = false;
1251            }
1252            mHadFling = false;
1253            mScrolledAfterDown = false;
1254            if (mFilmMode) {
1255                int xi = (int) (x + 0.5f);
1256                int yi = (int) (y + 0.5f);
1257                // We only care about being within the x bounds, necessary for
1258                // handling very wide images which are otherwise very hard to fling
1259                mTouchBoxIndex = mPositionController.hitTest(xi, getHeight() / 2);
1260
1261                if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
1262                    mTouchBoxIndex = Integer.MAX_VALUE;
1263                } else {
1264                    mTouchBoxDeletable =
1265                            mPictures.get(mTouchBoxIndex).isDeletable();
1266                }
1267            } else {
1268                mTouchBoxIndex = Integer.MAX_VALUE;
1269            }
1270        }
1271
1272        @Override
1273        public void onUp() {
1274            if (mIgnoreSwipingGesture) return;
1275
1276            mHolding &= ~HOLD_TOUCH_DOWN;
1277            mEdgeView.onRelease();
1278
1279            // If we scrolled in Y direction far enough, treat it as a delete
1280            // gesture.
1281            if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
1282                    && mTouchBoxIndex != Integer.MAX_VALUE) {
1283                Rect r = mPositionController.getPosition(mTouchBoxIndex);
1284                int h = getHeight();
1285                if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
1286                    int duration = mPositionController
1287                            .flingFilmY(mTouchBoxIndex, 0);
1288                    if (duration >= 0) {
1289                        mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
1290                        deleteAfterAnimation(duration);
1291                    }
1292                }
1293            }
1294
1295            if (mIgnoreUpEvent) {
1296                mIgnoreUpEvent = false;
1297                return;
1298            }
1299
1300            if (!(mFilmMode && !mHadFling && mFirstScrollX
1301                    && snapToNeighborImage())) {
1302                snapback();
1303            }
1304        }
1305
1306        public void setSwipingEnabled(boolean enabled) {
1307            mIgnoreSwipingGesture = !enabled;
1308        }
1309    }
1310
1311    public void setSwipingEnabled(boolean enabled) {
1312        mGestureListener.setSwipingEnabled(enabled);
1313    }
1314
1315    private void updateActionBar() {
1316        boolean isCamera = mPictures.get(0).isCamera();
1317        if (isCamera && !mFilmMode) {
1318            // Move into camera in page mode, lock
1319            mListener.onActionBarAllowed(false);
1320        } else {
1321            mListener.onActionBarAllowed(true);
1322            if (mFilmMode) mListener.onActionBarWanted();
1323        }
1324    }
1325
1326    public void setFilmMode(boolean enabled) {
1327        if (mFilmMode == enabled) return;
1328        mFilmMode = enabled;
1329        mPositionController.setFilmMode(mFilmMode);
1330        mModel.setNeedFullImage(!enabled);
1331        mModel.setFocusHintDirection(
1332                mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
1333        updateActionBar();
1334        mListener.onFilmModeChanged(enabled);
1335    }
1336
1337    public boolean getFilmMode() {
1338        return mFilmMode;
1339    }
1340
1341    ////////////////////////////////////////////////////////////////////////////
1342    //  Framework events
1343    ////////////////////////////////////////////////////////////////////////////
1344
1345    public void pause() {
1346        mPositionController.skipAnimation();
1347        mTileView.freeTextures();
1348        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
1349            mPictures.get(i).setScreenNail(null);
1350        }
1351        hideUndoBar();
1352    }
1353
1354    public void resume() {
1355        mTileView.prepareTextures();
1356        mPositionController.skipToFinalPosition();
1357    }
1358
1359    // move to the camera preview and show controls after resume
1360    public void resetToFirstPicture() {
1361        mModel.moveTo(0);
1362        setFilmMode(false);
1363    }
1364
1365    ////////////////////////////////////////////////////////////////////////////
1366    //  Undo Bar
1367    ////////////////////////////////////////////////////////////////////////////
1368
1369    private int mUndoBarState;
1370    private static final int UNDO_BAR_SHOW = 1;
1371    private static final int UNDO_BAR_TIMEOUT = 2;
1372    private static final int UNDO_BAR_TOUCHED = 4;
1373    private static final int UNDO_BAR_FULL_CAMERA = 8;
1374    private static final int UNDO_BAR_DELETE_LAST = 16;
1375
1376    // "deleteLast" means if the deletion is on the last remaining picture in
1377    // the album.
1378    private void showUndoBar(boolean deleteLast) {
1379        mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
1380        mUndoBarState = UNDO_BAR_SHOW;
1381        if(deleteLast) mUndoBarState |= UNDO_BAR_DELETE_LAST;
1382        mUndoBar.animateVisibility(GLView.VISIBLE);
1383        mHandler.sendEmptyMessageDelayed(MSG_UNDO_BAR_TIMEOUT, 3000);
1384        if (mListener != null) mListener.onUndoBarVisibilityChanged(true);
1385    }
1386
1387    private void hideUndoBar() {
1388        mHandler.removeMessages(MSG_UNDO_BAR_TIMEOUT);
1389        mListener.onCommitDeleteImage();
1390        mUndoBar.animateVisibility(GLView.INVISIBLE);
1391        mUndoBarState = 0;
1392        mUndoIndexHint = Integer.MAX_VALUE;
1393        mListener.onUndoBarVisibilityChanged(false);
1394    }
1395
1396    // Check if the one of the conditions for hiding the undo bar has been
1397    // met. The conditions are:
1398    //
1399    // 1. It has been three seconds since last showing, and (a) the user has
1400    // touched, or (b) the deleted picture is the last remaining picture in the
1401    // album.
1402    //
1403    // 2. The camera is shown in full screen.
1404    private void checkHideUndoBar(int addition) {
1405        mUndoBarState |= addition;
1406        if ((mUndoBarState & UNDO_BAR_SHOW) == 0) return;
1407        boolean timeout = (mUndoBarState & UNDO_BAR_TIMEOUT) != 0;
1408        boolean touched = (mUndoBarState & UNDO_BAR_TOUCHED) != 0;
1409        boolean fullCamera = (mUndoBarState & UNDO_BAR_FULL_CAMERA) != 0;
1410        boolean deleteLast = (mUndoBarState & UNDO_BAR_DELETE_LAST) != 0;
1411        if ((timeout && deleteLast) || fullCamera || touched) {
1412            hideUndoBar();
1413        }
1414    }
1415
1416    public boolean canUndo() {
1417        return (mUndoBarState & UNDO_BAR_SHOW) != 0;
1418    }
1419
1420    ////////////////////////////////////////////////////////////////////////////
1421    //  Rendering
1422    ////////////////////////////////////////////////////////////////////////////
1423
1424    @Override
1425    protected void render(GLCanvas canvas) {
1426        if (mFirst) {
1427            // Make sure the fields are properly initialized before checking
1428            // whether isCamera()
1429            mPictures.get(0).reload();
1430        }
1431        // Check if the camera preview occupies the full screen.
1432        boolean full = !mFilmMode && mPictures.get(0).isCamera()
1433                && mPositionController.isCenter()
1434                && mPositionController.isAtMinimalScale();
1435        if (mFirst || full != mFullScreenCamera) {
1436            mFullScreenCamera = full;
1437            mFirst = false;
1438            mListener.onFullScreenChanged(full);
1439            if (full) mHandler.sendEmptyMessage(MSG_UNDO_BAR_FULL_CAMERA);
1440        }
1441
1442        // Determine how many photos we need to draw in addition to the center
1443        // one.
1444        int neighbors;
1445        if (mFullScreenCamera) {
1446            neighbors = 0;
1447        } else {
1448            // In page mode, we draw only one previous/next photo. But if we are
1449            // doing capture animation, we want to draw all photos.
1450            boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
1451            boolean inCaptureAnimation =
1452                    ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
1453            if (inPageMode && !inCaptureAnimation) {
1454                neighbors = 1;
1455            } else {
1456                neighbors = SCREEN_NAIL_MAX;
1457            }
1458        }
1459
1460        // Draw photos from back to front
1461        for (int i = neighbors; i >= -neighbors; i--) {
1462            Rect r = mPositionController.getPosition(i);
1463            mPictures.get(i).draw(canvas, r);
1464        }
1465
1466        renderChild(canvas, mEdgeView);
1467        renderChild(canvas, mUndoBar);
1468
1469        mPositionController.advanceAnimation();
1470        checkFocusSwitching();
1471    }
1472
1473    ////////////////////////////////////////////////////////////////////////////
1474    //  Film mode focus switching
1475    ////////////////////////////////////////////////////////////////////////////
1476
1477    // Runs in GL thread.
1478    private void checkFocusSwitching() {
1479        if (!mFilmMode) return;
1480        if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
1481        if (switchPosition() != 0) {
1482            mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
1483        }
1484    }
1485
1486    // Runs in main thread.
1487    private void switchFocus() {
1488        if (mHolding != 0) return;
1489        switch (switchPosition()) {
1490            case -1:
1491                switchToPrevImage();
1492                break;
1493            case 1:
1494                switchToNextImage();
1495                break;
1496        }
1497    }
1498
1499    // Returns -1 if we should switch focus to the previous picture, +1 if we
1500    // should switch to the next, 0 otherwise.
1501    private int switchPosition() {
1502        Rect curr = mPositionController.getPosition(0);
1503        int center = getWidth() / 2;
1504
1505        if (curr.left > center && mPrevBound < 0) {
1506            Rect prev = mPositionController.getPosition(-1);
1507            int currDist = curr.left - center;
1508            int prevDist = center - prev.right;
1509            if (prevDist < currDist) {
1510                return -1;
1511            }
1512        } else if (curr.right < center && mNextBound > 0) {
1513            Rect next = mPositionController.getPosition(1);
1514            int currDist = center - curr.right;
1515            int nextDist = next.left - center;
1516            if (nextDist < currDist) {
1517                return 1;
1518            }
1519        }
1520
1521        return 0;
1522    }
1523
1524    // Switch to the previous or next picture if the hit position is inside
1525    // one of their boxes. This runs in main thread.
1526    private void switchToHitPicture(int x, int y) {
1527        if (mPrevBound < 0) {
1528            Rect r = mPositionController.getPosition(-1);
1529            if (r.right >= x) {
1530                slideToPrevPicture();
1531                return;
1532            }
1533        }
1534
1535        if (mNextBound > 0) {
1536            Rect r = mPositionController.getPosition(1);
1537            if (r.left <= x) {
1538                slideToNextPicture();
1539                return;
1540            }
1541        }
1542    }
1543
1544    ////////////////////////////////////////////////////////////////////////////
1545    //  Page mode focus switching
1546    //
1547    //  We slide image to the next one or the previous one in two cases: 1: If
1548    //  the user did a fling gesture with enough velocity.  2 If the user has
1549    //  moved the picture a lot.
1550    ////////////////////////////////////////////////////////////////////////////
1551
1552    private boolean swipeImages(float velocityX, float velocityY) {
1553        if (mFilmMode) return false;
1554
1555        // Avoid swiping images if we're possibly flinging to view the
1556        // zoomed in picture vertically.
1557        PositionController controller = mPositionController;
1558        boolean isMinimal = controller.isAtMinimalScale();
1559        int edges = controller.getImageAtEdges();
1560        if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
1561            if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
1562                    || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
1563                return false;
1564
1565        // If we are at the edge of the current photo and the sweeping velocity
1566        // exceeds the threshold, slide to the next / previous image.
1567        if (velocityX < -SWIPE_THRESHOLD && (isMinimal
1568                || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
1569            return slideToNextPicture();
1570        } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
1571                || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
1572            return slideToPrevPicture();
1573        }
1574
1575        return false;
1576    }
1577
1578    private void snapback() {
1579        if ((mHolding & ~HOLD_DELETE) != 0) return;
1580        if (mFilmMode || !snapToNeighborImage()) {
1581            mPositionController.snapback();
1582        }
1583    }
1584
1585    private boolean snapToNeighborImage() {
1586        Rect r = mPositionController.getPosition(0);
1587        int viewW = getWidth();
1588        // Setting the move threshold proportional to the width of the view
1589        int moveThreshold = viewW / 5 ;
1590        int threshold = moveThreshold + gapToSide(r.width(), viewW);
1591
1592        // If we have moved the picture a lot, switching.
1593        if (viewW - r.right > threshold) {
1594            return slideToNextPicture();
1595        } else if (r.left > threshold) {
1596            return slideToPrevPicture();
1597        }
1598
1599        return false;
1600    }
1601
1602    private boolean slideToNextPicture() {
1603        if (mNextBound <= 0) return false;
1604        switchToNextImage();
1605        mPositionController.startHorizontalSlide();
1606        return true;
1607    }
1608
1609    private boolean slideToPrevPicture() {
1610        if (mPrevBound >= 0) return false;
1611        switchToPrevImage();
1612        mPositionController.startHorizontalSlide();
1613        return true;
1614    }
1615
1616    private static int gapToSide(int imageWidth, int viewWidth) {
1617        return Math.max(0, (viewWidth - imageWidth) / 2);
1618    }
1619
1620    ////////////////////////////////////////////////////////////////////////////
1621    //  Focus switching
1622    ////////////////////////////////////////////////////////////////////////////
1623
1624    public void switchToImage(int index) {
1625        mModel.moveTo(index);
1626    }
1627
1628    private void switchToNextImage() {
1629        mModel.moveTo(mModel.getCurrentIndex() + 1);
1630    }
1631
1632    private void switchToPrevImage() {
1633        mModel.moveTo(mModel.getCurrentIndex() - 1);
1634    }
1635
1636    private void switchToFirstImage() {
1637        mModel.moveTo(0);
1638    }
1639
1640    ////////////////////////////////////////////////////////////////////////////
1641    //  Opening Animation
1642    ////////////////////////////////////////////////////////////////////////////
1643
1644    public void setOpenAnimationRect(Rect rect) {
1645        mPositionController.setOpenAnimationRect(rect);
1646    }
1647
1648    ////////////////////////////////////////////////////////////////////////////
1649    //  Capture Animation
1650    ////////////////////////////////////////////////////////////////////////////
1651
1652    public boolean switchWithCaptureAnimation(int offset) {
1653        GLRoot root = getGLRoot();
1654        if(root == null) return false;
1655        root.lockRenderThread();
1656        try {
1657            return switchWithCaptureAnimationLocked(offset);
1658        } finally {
1659            root.unlockRenderThread();
1660        }
1661    }
1662
1663    private boolean switchWithCaptureAnimationLocked(int offset) {
1664        if (mHolding != 0) return true;
1665        if (offset == 1) {
1666            if (mNextBound <= 0) return false;
1667            // Temporary disable action bar until the capture animation is done.
1668            if (!mFilmMode) mListener.onActionBarAllowed(false);
1669            switchToNextImage();
1670            mPositionController.startCaptureAnimationSlide(-1);
1671        } else if (offset == -1) {
1672            if (mPrevBound >= 0) return false;
1673            if (mFilmMode) setFilmMode(false);
1674
1675            // If we are too far away from the first image (so that we don't
1676            // have all the ScreenNails in-between), we go directly without
1677            // animation.
1678            if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
1679                switchToFirstImage();
1680                mPositionController.skipToFinalPosition();
1681                return true;
1682            }
1683
1684            switchToFirstImage();
1685            mPositionController.startCaptureAnimationSlide(1);
1686        } else {
1687            return false;
1688        }
1689        mHolding |= HOLD_CAPTURE_ANIMATION;
1690        Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
1691        mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
1692        return true;
1693    }
1694
1695    private void captureAnimationDone(int offset) {
1696        mHolding &= ~HOLD_CAPTURE_ANIMATION;
1697        if (offset == 1 && !mFilmMode) {
1698            // Now the capture animation is done, enable the action bar.
1699            mListener.onActionBarAllowed(true);
1700            mListener.onActionBarWanted();
1701        }
1702        snapback();
1703    }
1704
1705    ////////////////////////////////////////////////////////////////////////////
1706    //  Card deck effect calculation
1707    ////////////////////////////////////////////////////////////////////////////
1708
1709    // Returns the scrolling progress value for an object moving out of a
1710    // view. The progress value measures how much the object has moving out of
1711    // the view. The object currently displays in [left, right), and the view is
1712    // at [0, viewWidth].
1713    //
1714    // The returned value is negative when the object is moving right, and
1715    // positive when the object is moving left. The value goes to -1 or 1 when
1716    // the object just moves out of the view completely. The value is 0 if the
1717    // object currently fills the view.
1718    private static float calculateMoveOutProgress(int left, int right,
1719            int viewWidth) {
1720        // w = object width
1721        // viewWidth = view width
1722        int w = right - left;
1723
1724        // If the object width is smaller than the view width,
1725        //      |....view....|
1726        //                   |<-->|      progress = -1 when left = viewWidth
1727        //          |<-->|               progress = 0 when left = viewWidth / 2 - w / 2
1728        // |<-->|                        progress = 1 when left = -w
1729        if (w < viewWidth) {
1730            int zx = viewWidth / 2 - w / 2;
1731            if (left > zx) {
1732                return -(left - zx) / (float) (viewWidth - zx);  // progress = (0, -1]
1733            } else {
1734                return (left - zx) / (float) (-w - zx);  // progress = [0, 1]
1735            }
1736        }
1737
1738        // If the object width is larger than the view width,
1739        //             |..view..|
1740        //                      |<--------->| progress = -1 when left = viewWidth
1741        //             |<--------->|          progress = 0 between left = 0
1742        //          |<--------->|                          and right = viewWidth
1743        // |<--------->|                      progress = 1 when right = 0
1744        if (left > 0) {
1745            return -left / (float) viewWidth;
1746        }
1747
1748        if (right < viewWidth) {
1749            return (viewWidth - right) / (float) viewWidth;
1750        }
1751
1752        return 0;
1753    }
1754
1755    // Maps a scrolling progress value to the alpha factor in the fading
1756    // animation.
1757    private float getScrollAlpha(float scrollProgress) {
1758        return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
1759                     1 - Math.abs(scrollProgress)) : 1.0f;
1760    }
1761
1762    // Maps a scrolling progress value to the scaling factor in the fading
1763    // animation.
1764    private float getScrollScale(float scrollProgress) {
1765        float interpolatedProgress = mScaleInterpolator.getInterpolation(
1766                Math.abs(scrollProgress));
1767        float scale = (1 - interpolatedProgress) +
1768                interpolatedProgress * TRANSITION_SCALE_FACTOR;
1769        return scale;
1770    }
1771
1772
1773    // This interpolator emulates the rate at which the perceived scale of an
1774    // object changes as its distance from a camera increases. When this
1775    // interpolator is applied to a scale animation on a view, it evokes the
1776    // sense that the object is shrinking due to moving away from the camera.
1777    private static class ZInterpolator {
1778        private float focalLength;
1779
1780        public ZInterpolator(float foc) {
1781            focalLength = foc;
1782        }
1783
1784        public float getInterpolation(float input) {
1785            return (1.0f - focalLength / (focalLength + input)) /
1786                (1.0f - focalLength / (focalLength + 1.0f));
1787        }
1788    }
1789
1790    // Returns an interpolated value for the page/film transition.
1791    // When ratio = 0, the result is from.
1792    // When ratio = 1, the result is to.
1793    private static float interpolate(float ratio, float from, float to) {
1794        return from + (to - from) * ratio * ratio;
1795    }
1796
1797    // Returns the alpha factor in film mode if a picture is not in the center.
1798    // The 0.03 lower bound is to make the item always visible a bit.
1799    private float getOffsetAlpha(float offset) {
1800        offset /= 0.5f;
1801        float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
1802        return Utils.clamp(alpha, 0.03f, 1f);
1803    }
1804
1805    ////////////////////////////////////////////////////////////////////////////
1806    //  Simple public utilities
1807    ////////////////////////////////////////////////////////////////////////////
1808
1809    public void setListener(Listener listener) {
1810        mListener = listener;
1811    }
1812
1813    public Rect getPhotoRect(int index) {
1814        return mPositionController.getPosition(index);
1815    }
1816
1817    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
1818        Rect location = new Rect();
1819        Utils.assertTrue(root.getBoundsOf(this, location));
1820
1821        Rect fullRect = bounds();
1822        PhotoFallbackEffect effect = new PhotoFallbackEffect();
1823        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
1824            MediaItem item = mModel.getMediaItem(i);
1825            if (item == null) continue;
1826            ScreenNail sc = mModel.getScreenNail(i);
1827            if (!(sc instanceof TiledScreenNail)
1828                    || ((TiledScreenNail) sc).isShowingPlaceholder()) continue;
1829
1830            // Now, sc is BitmapScreenNail and is not showing placeholder
1831            Rect rect = new Rect(getPhotoRect(i));
1832            if (!Rect.intersects(fullRect, rect)) continue;
1833            rect.offset(location.left, location.top);
1834
1835            int width = sc.getWidth();
1836            int height = sc.getHeight();
1837
1838            int rotation = mModel.getImageRotation(i);
1839            RawTexture texture;
1840            if ((rotation % 180) == 0) {
1841                texture = new RawTexture(width, height, true);
1842                canvas.beginRenderTarget(texture);
1843                canvas.translate(width / 2f, height / 2f);
1844            } else {
1845                texture = new RawTexture(height, width, true);
1846                canvas.beginRenderTarget(texture);
1847                canvas.translate(height / 2f, width / 2f);
1848            }
1849
1850            canvas.rotate(rotation, 0, 0, 1);
1851            canvas.translate(-width / 2f, -height / 2f);
1852            sc.draw(canvas, 0, 0, width, height);
1853            canvas.endRenderTarget();
1854            effect.addEntry(item.getPath(), rect, texture);
1855        }
1856        return effect;
1857    }
1858}
1859