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