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