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