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