PhotoView.java revision 616a70fdb4473d2fbd7b70772a3a82b908aeae1e
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.view.MotionEvent;
26import android.view.animation.AccelerateInterpolator;
27
28import com.android.gallery3d.R;
29import com.android.gallery3d.app.GalleryActivity;
30import com.android.gallery3d.common.Utils;
31import com.android.gallery3d.data.MediaItem;
32import com.android.gallery3d.data.MediaObject;
33import com.android.gallery3d.util.RangeArray;
34
35public class PhotoView extends GLView {
36    @SuppressWarnings("unused")
37    private static final String TAG = "PhotoView";
38    private static final int PLACEHOLDER_COLOR = 0xFF222222;
39
40    public static final int INVALID_SIZE = -1;
41    public static final long INVALID_DATA_VERSION =
42            MediaObject.INVALID_DATA_VERSION;
43
44    public static class Size {
45        public int width;
46        public int height;
47    }
48
49    public interface Model extends TileImageView.Model {
50        public int getCurrentIndex();
51        public void moveTo(int index);
52
53        // Returns the size for the specified picture. If the size information is
54        // not avaiable, width = height = 0.
55        public void getImageSize(int offset, Size size);
56
57        // Returns the media item for the specified picture.
58        public MediaItem getMediaItem(int offset);
59
60        // Returns the rotation for the specified picture.
61        public int getImageRotation(int offset);
62
63        // This amends the getScreenNail() method of TileImageView.Model to get
64        // ScreenNail at previous (negative offset) or next (positive offset)
65        // positions. Returns null if the specified ScreenNail is unavailable.
66        public ScreenNail getScreenNail(int offset);
67
68        // Set this to true if we need the model to provide full images.
69        public void setNeedFullImage(boolean enabled);
70
71        // Returns true if the item is the Camera preview.
72        public boolean isCamera(int offset);
73
74        // Returns true if the item is a Video.
75        public boolean isVideo(int offset);
76    }
77
78    public interface Listener {
79        public void onSingleTapUp(int x, int y);
80        public void lockOrientation();
81        public void unlockOrientation();
82        public void onFullScreenChanged(boolean full);
83        public void onActionBarAllowed(boolean allowed);
84        public void onCurrentImageUpdated();
85    }
86
87    // Here is a graph showing the places we need to lock/unlock device
88    // orientation:
89    //
90    //           +------------+ A  +------------+
91    // Page mode |   Camera   |<---|   Photo    |
92    //           |  [locked]  |--->| [unlocked] |
93    //           +------------+  B +------------+
94    //                ^                  ^
95    //                | C                | D
96    //           +------------+    +------------+
97    //           |   Camera   |    |   Photo    |
98    // Film mode |    [*]     |    |    [*]     |
99    //           +------------+    +------------+
100    //
101    // In Page mode, we want to lock in Camera because we don't want the system
102    // rotation animation. We also want to unlock in Photo because we want to
103    // show the system action bar in the right place.
104    //
105    // We don't show action bar in Film mode, so it's fine for it to be locked
106    // or unlocked in Film mode.
107    //
108    // There are four transitions we need to check if we need to
109    // lock/unlock. Marked as A to D above and in the code.
110
111    private static final int MSG_SHOW_LOADING = 1;
112    private static final int MSG_CANCEL_EXTRA_SCALING = 2;
113    private static final int MSG_SWITCH_FOCUS = 3;
114    private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
115
116    private static final long DELAY_SHOW_LOADING = 250; // 250ms;
117
118    private static final int LOADING_INIT = 0;
119    private static final int LOADING_TIMEOUT = 1;
120    private static final int LOADING_COMPLETE = 2;
121    private static final int LOADING_FAIL = 3;
122
123    private static final int MOVE_THRESHOLD = 256;
124    private static final float SWIPE_THRESHOLD = 300f;
125
126    private static final float DEFAULT_TEXT_SIZE = 20;
127    private static float TRANSITION_SCALE_FACTOR = 0.74f;
128    private static final int ICON_RATIO = 6;
129
130    // whether we want to apply card deck effect in page mode.
131    private static final boolean CARD_EFFECT = true;
132
133    // Used to calculate the scaling factor for the fading animation.
134    private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
135
136    // Used to calculate the alpha factor for the fading animation.
137    private AccelerateInterpolator mAlphaInterpolator =
138            new AccelerateInterpolator(0.9f);
139
140    // We keep this many previous ScreenNails. (also this many next ScreenNails)
141    public static final int SCREEN_NAIL_MAX = 3;
142
143    // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
144    // SCREEN_NAIL_MAX.
145    private final RangeArray<Picture> mPictures =
146            new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
147
148    private final MyGestureListener mGestureListener;
149    private final GestureRecognizer mGestureRecognizer;
150    private final PositionController mPositionController;
151
152    private Listener mListener;
153    private Model mModel;
154    private StringTexture mLoadingText;
155    private StringTexture mNoThumbnailText;
156    private TileImageView mTileView;
157    private EdgeView mEdgeView;
158    private Texture mVideoPlayIcon;
159
160    private ProgressSpinner mLoadingSpinner;
161
162    private SynchronizedHandler mHandler;
163
164    private int mLoadingState = LOADING_COMPLETE;
165
166    private Point mImageCenter = new Point();
167    private boolean mCancelExtraScalingPending;
168    private boolean mFilmMode = false;
169    private int mDisplayRotation = 0;
170    private int mCompensation = 0;
171    private boolean mFullScreen = true;
172    private Rect mCameraRelativeFrame = new Rect();
173    private Rect mCameraRect = new Rect();
174
175    // [mPrevBound, mNextBound] is the range of index for all pictures in the
176    // model, if we assume the index of current focused picture is 0.  So if
177    // there are some previous pictures, mPrevBound < 0, and if there are some
178    // next pictures, mNextBound > 0.
179    private int mPrevBound;
180    private int mNextBound;
181
182    // This variable prevents us doing snapback until its values goes to 0. This
183    // happens if the user gesture is still in progress or we are in a capture
184    // animation.
185    private int mHolding;
186    private static final int HOLD_TOUCH_DOWN = 1;
187    private static final int HOLD_CAPTURE_ANIMATION = 2;
188
189    public PhotoView(GalleryActivity activity) {
190        mTileView = new TileImageView(activity);
191        addComponent(mTileView);
192        Context context = activity.getAndroidContext();
193        mEdgeView = new EdgeView(context);
194        addComponent(mEdgeView);
195        mLoadingSpinner = new ProgressSpinner(context);
196        mLoadingText = StringTexture.newInstance(
197                context.getString(R.string.loading),
198                DEFAULT_TEXT_SIZE, Color.WHITE);
199        mNoThumbnailText = StringTexture.newInstance(
200                context.getString(R.string.no_thumbnail),
201                DEFAULT_TEXT_SIZE, Color.WHITE);
202
203        mHandler = new MyHandler(activity.getGLRoot());
204
205        mGestureListener = new MyGestureListener();
206        mGestureRecognizer = new GestureRecognizer(context, mGestureListener);
207
208        mPositionController = new PositionController(context,
209                new PositionController.Listener() {
210                    public void invalidate() {
211                        PhotoView.this.invalidate();
212                    }
213                    public boolean isHolding() {
214                        return mHolding != 0;
215                    }
216                    public void onPull(int offset, int direction) {
217                        mEdgeView.onPull(offset, direction);
218                    }
219                    public void onRelease() {
220                        mEdgeView.onRelease();
221                    }
222                    public void onAbsorb(int velocity, int direction) {
223                        mEdgeView.onAbsorb(velocity, direction);
224                    }
225                });
226        mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
227        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
228            if (i == 0) {
229                mPictures.put(i, new FullPicture());
230            } else {
231                mPictures.put(i, new ScreenNailPicture(i));
232            }
233        }
234    }
235
236    public void setModel(Model model) {
237        mModel = model;
238        mTileView.setModel(mModel);
239    }
240
241    class MyHandler extends SynchronizedHandler {
242        public MyHandler(GLRoot root) {
243            super(root);
244        }
245
246        @Override
247        public void handleMessage(Message message) {
248            switch (message.what) {
249                case MSG_SHOW_LOADING: {
250                    if (mLoadingState == LOADING_INIT) {
251                        // We don't need the opening animation
252                        mPositionController.setOpenAnimationRect(null);
253
254                        mLoadingSpinner.startAnimation();
255                        mLoadingState = LOADING_TIMEOUT;
256                        invalidate();
257                    }
258                    break;
259                }
260                case MSG_CANCEL_EXTRA_SCALING: {
261                    mGestureRecognizer.cancelScale();
262                    mPositionController.setExtraScalingRange(false);
263                    mCancelExtraScalingPending = false;
264                    break;
265                }
266                case MSG_SWITCH_FOCUS: {
267                    switchFocus();
268                    break;
269                }
270                case MSG_CAPTURE_ANIMATION_DONE: {
271                    // message.arg1 is the offset parameter passed to
272                    // switchWithCaptureAnimation().
273                    captureAnimationDone(message.arg1);
274                    break;
275                }
276                default: throw new AssertionError(message.what);
277            }
278        }
279    };
280
281    private void updateLoadingState() {
282        // Possible transitions of mLoadingState:
283        //        INIT --> TIMEOUT, COMPLETE, FAIL
284        //     TIMEOUT --> COMPLETE, FAIL, INIT
285        //    COMPLETE --> INIT
286        //        FAIL --> INIT
287        if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
288            mHandler.removeMessages(MSG_SHOW_LOADING);
289            mLoadingState = LOADING_COMPLETE;
290        } else if (mModel.isFailedToLoad()) {
291            mHandler.removeMessages(MSG_SHOW_LOADING);
292            mLoadingState = LOADING_FAIL;
293            // We don't want the opening animation after loading failure
294            mPositionController.setOpenAnimationRect(null);
295        } else if (mLoadingState != LOADING_INIT) {
296            mLoadingState = LOADING_INIT;
297            mHandler.removeMessages(MSG_SHOW_LOADING);
298            mHandler.sendEmptyMessageDelayed(
299                    MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
300        }
301    }
302
303    ////////////////////////////////////////////////////////////////////////////
304    //  Data/Image change notifications
305    ////////////////////////////////////////////////////////////////////////////
306
307    public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
308        mPrevBound = prevBound;
309        mNextBound = nextBound;
310
311        // Move the boxes
312        mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
313                mModel.isCamera(0));
314
315        // Update the ScreenNails.
316        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
317            mPictures.get(i).reload();
318        }
319
320        invalidate();
321    }
322
323    public void notifyImageChange(int index) {
324        if (index == 0) {
325            mListener.onCurrentImageUpdated();
326        }
327        mPictures.get(index).reload();
328        invalidate();
329    }
330
331    @Override
332    protected void onLayout(
333            boolean changeSize, int left, int top, int right, int bottom) {
334        int w = right - left;
335        int h = bottom - top;
336        mTileView.layout(0, 0, w, h);
337        mEdgeView.layout(0, 0, w, h);
338
339        GLRoot root = getGLRoot();
340        int displayRotation = root.getDisplayRotation();
341        int compensation = root.getCompensation();
342        if (mDisplayRotation != displayRotation
343                || mCompensation != compensation) {
344            mDisplayRotation = displayRotation;
345            mCompensation = compensation;
346
347            // We need to change the size and rotation of the Camera ScreenNail,
348            // but we don't want it to animate because the size doen't actually
349            // change in the eye of the user.
350            for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
351                Picture p = mPictures.get(i);
352                if (p.isCamera()) {
353                    p.updateSize(true);
354                }
355            }
356        }
357
358        updateConstrainedFrame();
359        if (changeSize) {
360            mPositionController.setViewSize(getWidth(), getHeight());
361        }
362    }
363
364    // Update the constrained frame due to layout change.
365    private void updateConstrainedFrame() {
366        // Get the width and height in framework orientation because the given
367        // mCameraRelativeFrame is in that coordinates.
368        int w = getWidth();
369        int h = getHeight();
370        if (mCompensation % 180 != 0) {
371            int tmp = w;
372            w = h;
373            h = tmp;
374        }
375        int l = mCameraRelativeFrame.left;
376        int t = mCameraRelativeFrame.top;
377        int r = mCameraRelativeFrame.right;
378        int b = mCameraRelativeFrame.bottom;
379
380        // Now convert it to the coordinates we are using.
381        switch (mCompensation) {
382            case 0: mCameraRect.set(l, t, r, b); break;
383            case 90: mCameraRect.set(h - b, l, h - t, r); break;
384            case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
385            case 270: mCameraRect.set(t, w - r, b, w - l); break;
386        }
387
388        Log.d(TAG, "compensation = " + mCompensation
389                + ", CameraRelativeFrame = " + mCameraRelativeFrame
390                + ", mCameraRect = " + mCameraRect);
391        mPositionController.setConstrainedFrame(mCameraRect);
392    }
393
394    public void setCameraRelativeFrame(Rect frame) {
395        mCameraRelativeFrame.set(frame);
396        updateConstrainedFrame();
397    }
398
399    // Returns the rotation we need to do to the camera texture before drawing
400    // it to the canvas, assuming the camera texture is correct when the device
401    // is in its natural orientation.
402    private int getCameraRotation() {
403        return (mCompensation - mDisplayRotation + 360) % 360;
404    }
405
406    ////////////////////////////////////////////////////////////////////////////
407    //  Pictures
408    ////////////////////////////////////////////////////////////////////////////
409
410    private interface Picture {
411        void reload();
412        void draw(GLCanvas canvas, Rect r);
413        void setScreenNail(ScreenNail s);
414        boolean isCamera();  // whether the picture is a camera preview
415        void updateSize(boolean force);  // called when mCompensation changes
416    };
417
418    class FullPicture implements Picture {
419        private int mRotation;
420        private boolean mIsCamera;
421        private boolean mIsVideo;
422        private boolean mWasCameraCenter;
423
424        public void FullPicture(TileImageView tileView) {
425            mTileView = tileView;
426        }
427
428        @Override
429        public void reload() {
430            // mImageWidth and mImageHeight will get updated
431            mTileView.notifyModelInvalidated();
432
433            mIsCamera = mModel.isCamera(0);
434            mIsVideo = mModel.isVideo(0);
435            setScreenNail(mModel.getScreenNail(0));
436            updateSize(false);
437            updateLoadingState();
438        }
439
440        @Override
441        public void updateSize(boolean force) {
442            if (mIsCamera) {
443                mRotation = getCameraRotation();
444            } else {
445                mRotation = mModel.getImageRotation(0);
446            }
447
448            int w = mTileView.mImageWidth;
449            int h = mTileView.mImageHeight;
450            mPositionController.setImageSize(0,
451                    getRotated(mRotation, w, h),
452                    getRotated(mRotation, h, w),
453                    force);
454        }
455
456        @Override
457        public void draw(GLCanvas canvas, Rect r) {
458            boolean isCenter = mPositionController.isCenter();
459
460            if (mLoadingState == LOADING_COMPLETE) {
461                drawTileView(canvas, r);
462            }
463            renderMessage(canvas, r.centerX(), r.centerY());
464
465            if (mIsCamera) {
466                boolean full = !mFilmMode && isCenter
467                        && mPositionController.isAtMinimalScale();
468                if (full != mFullScreen) {
469                    mFullScreen = full;
470                    mListener.onFullScreenChanged(full);
471                }
472            }
473
474            // We want to have the following transitions:
475            // (1) Move camera preview out of its place: switch to film mode
476            // (2) Move camera preview into its place: switch to page mode
477            // The extra mWasCenter check makes sure (1) does not apply if in
478            // page mode, we move _to_ the camera preview from another picture.
479
480            // Holdings except touch-down prevent the transitions.
481            if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
482
483            boolean isCameraCenter = mIsCamera && isCenter;
484
485            if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) {
486                // Temporary disabled to de-emphasize filmstrip.
487                // setFilmMode(true);
488            } else if (!mWasCameraCenter && isCameraCenter && mFilmMode) {
489                setFilmMode(false);
490            }
491
492            if (isCenter && !mFilmMode) {
493                if (mIsCamera) {
494                    // move into camera, lock
495                    mListener.lockOrientation();  // Transition A
496                } else {
497                    // move out of camera, unlock
498                    mListener.unlockOrientation();  // Transition B
499                }
500            }
501
502            mWasCameraCenter = isCameraCenter;
503        }
504
505        @Override
506        public void setScreenNail(ScreenNail s) {
507            mTileView.setScreenNail(s);
508        }
509
510        @Override
511        public boolean isCamera() {
512            return mIsCamera;
513        }
514
515        private void drawTileView(GLCanvas canvas, Rect r) {
516            float imageScale = mPositionController.getImageScale();
517            int viewW = getWidth();
518            int viewH = getHeight();
519            float cx = r.exactCenterX();
520            float cy = r.exactCenterY();
521            float scale = 1f;  // the scaling factor due to card effect
522
523            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
524            float filmRatio = mPositionController.getFilmRatio();
525            boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
526                    && filmRatio != 1f && !mPictures.get(-1).isCamera()
527                    && !mPositionController.inOpeningAnimation();
528            if (wantsCardEffect) {
529                // Calculate the move-out progress value.
530                int left = r.left;
531                int right = r.right;
532                float progress = calculateMoveOutProgress(left, right, viewW);
533                progress = Utils.clamp(progress, -1f, 1f);
534
535                // We only want to apply the fading animation if the scrolling
536                // movement is to the right.
537                if (progress < 0) {
538                    scale = getScrollScale(progress);
539                    float alpha = getScrollAlpha(progress);
540                    scale = interpolate(filmRatio, scale, 1f);
541                    alpha = interpolate(filmRatio, alpha, 1f);
542
543                    imageScale *= scale;
544                    canvas.multiplyAlpha(alpha);
545
546                    float cxPage;  // the cx value in page mode
547                    if (right - left <= viewW) {
548                        // If the picture is narrower than the view, keep it at
549                        // the center of the view.
550                        cxPage = viewW / 2f;
551                    } else {
552                        // If the picture is wider than the view (it's
553                        // zoomed-in), keep the left edge of the object align
554                        // the the left edge of the view.
555                        cxPage = (right - left) * scale / 2f;
556                    }
557                    cx = interpolate(filmRatio, cxPage, cx);
558                }
559            }
560
561            // Draw the tile view.
562            setTileViewPosition(cx, cy, viewW, viewH, imageScale);
563            PhotoView.super.render(canvas);
564
565            // Draw the play video icon.
566            if (mIsVideo) {
567                canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
568                int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
569                drawVideoPlayIcon(canvas, s);
570            }
571
572            canvas.restore();
573        }
574
575        // Set the position of the tile view
576        private void setTileViewPosition(float cx, float cy,
577                int viewW, int viewH, float scale) {
578            // Find out the bitmap coordinates of the center of the view
579            int imageW = mPositionController.getImageWidth();
580            int imageH = mPositionController.getImageHeight();
581            int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
582            int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
583
584            int inverseX = imageW - centerX;
585            int inverseY = imageH - centerY;
586            int x, y;
587            switch (mRotation) {
588                case 0: x = centerX; y = centerY; break;
589                case 90: x = centerY; y = inverseX; break;
590                case 180: x = inverseX; y = inverseY; break;
591                case 270: x = inverseY; y = centerX; break;
592                default:
593                    throw new RuntimeException(String.valueOf(mRotation));
594            }
595            mTileView.setPosition(x, y, scale, mRotation);
596        }
597
598        private void renderMessage(GLCanvas canvas, int x, int y) {
599            // Draw the progress spinner and the text below it
600            //
601            // (x, y) is where we put the center of the spinner.
602            // s is the size of the video play icon, and we use s to layout text
603            // because we want to keep the text at the same place when the video
604            // play icon is shown instead of the spinner.
605            int w = getWidth();
606            int h = getHeight();
607            int s = Math.min(w, h) / ICON_RATIO;
608
609            if (mLoadingState == LOADING_TIMEOUT) {
610                StringTexture m = mLoadingText;
611                ProgressSpinner p = mLoadingSpinner;
612                p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2);
613                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
614                invalidate(); // we need to keep the spinner rotating
615            } else if (mLoadingState == LOADING_FAIL) {
616                StringTexture m = mNoThumbnailText;
617                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
618            }
619
620            // Draw a debug indicator showing which picture has focus (index ==
621            // 0).
622            // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
623        }
624    }
625
626    private class ScreenNailPicture implements Picture {
627        private int mIndex;
628        private int mRotation;
629        private ScreenNail mScreenNail;
630        private Size mSize = new Size();
631        private boolean mIsCamera;
632        private boolean mIsVideo;
633
634        public ScreenNailPicture(int index) {
635            mIndex = index;
636        }
637
638        @Override
639        public void reload() {
640            mIsCamera = mModel.isCamera(mIndex);
641            mIsVideo = mModel.isVideo(mIndex);
642            setScreenNail(mModel.getScreenNail(mIndex));
643        }
644
645        @Override
646        public void draw(GLCanvas canvas, Rect r) {
647            if (mScreenNail == null) {
648                // Draw a placeholder rectange if there will be a picture in
649                // this position.
650                if (mIndex >= mPrevBound && mIndex <= mNextBound) {
651                    canvas.fillRect(r.left, r.top, r.width(), r.height(),
652                            PLACEHOLDER_COLOR);
653                }
654                return;
655            }
656            if (r.left >= getWidth() || r.right <= 0 ||
657                    r.top >= getHeight() || r.bottom <= 0) {
658                mScreenNail.noDraw();
659                return;
660            }
661
662            if (mIsCamera && mFullScreen != false) {
663                mFullScreen = false;
664                mListener.onFullScreenChanged(false);
665            }
666
667            float filmRatio = mPositionController.getFilmRatio();
668            boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
669                    && filmRatio != 1f && !mPictures.get(0).isCamera();
670            int w = getWidth();
671            int cx = wantsCardEffect
672                    ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
673                    : r.centerX();
674            int cy = r.centerY();
675            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
676            canvas.translate(cx, cy);
677            if (wantsCardEffect) {
678                float progress = (float) (w / 2 - r.centerX()) / w;
679                progress = Utils.clamp(progress, -1, 1);
680                float alpha = getScrollAlpha(progress);
681                float scale = getScrollScale(progress);
682                alpha = interpolate(filmRatio, alpha, 1f);
683                scale = interpolate(filmRatio, scale, 1f);
684                canvas.multiplyAlpha(alpha);
685                canvas.scale(scale, scale, 1);
686            }
687            if (mRotation != 0) {
688                canvas.rotate(mRotation, 0, 0, 1);
689            }
690            int drawW = getRotated(mRotation, r.width(), r.height());
691            int drawH = getRotated(mRotation, r.height(), r.width());
692            mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
693            if (mIsVideo) drawVideoPlayIcon(canvas, Math.min(drawW, drawH));
694            canvas.restore();
695        }
696
697        @Override
698        public void setScreenNail(ScreenNail s) {
699            if (mScreenNail == s) return;
700            mScreenNail = s;
701            updateSize(false);
702        }
703
704        @Override
705        public void updateSize(boolean force) {
706            if (mIsCamera) {
707                mRotation = getCameraRotation();
708            } else {
709                mRotation = mModel.getImageRotation(mIndex);
710            }
711
712            int w = 0, h = 0;
713            if (mScreenNail != null) {
714                w = mScreenNail.getWidth();
715                h = mScreenNail.getHeight();
716            } else if (mModel != null) {
717                // If we don't have ScreenNail available, we can still try to
718                // get the size information of it.
719                mModel.getImageSize(mIndex, mSize);
720                w = mSize.width;
721                h = mSize.height;
722            }
723
724            if (w != 0 && h != 0)  {
725                mPositionController.setImageSize(mIndex,
726                        getRotated(mRotation, w, h),
727                        getRotated(mRotation, h, w),
728                        force);
729            }
730        }
731
732        @Override
733        public boolean isCamera() {
734            return mIsCamera;
735        }
736    }
737
738    // Draw the video play icon (in the place where the spinner was)
739    private void drawVideoPlayIcon(GLCanvas canvas, int side) {
740        int s = side / ICON_RATIO;
741        // Draw the video play icon at the center
742        mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
743    }
744
745    private static int getRotated(int degree, int original, int theother) {
746        return (degree % 180 == 0) ? original : theother;
747    }
748
749    ////////////////////////////////////////////////////////////////////////////
750    //  Gestures Handling
751    ////////////////////////////////////////////////////////////////////////////
752
753    @Override
754    protected boolean onTouch(MotionEvent event) {
755        mGestureRecognizer.onTouchEvent(event);
756        return true;
757    }
758
759    private class MyGestureListener implements GestureRecognizer.Listener {
760        private boolean mIgnoreUpEvent = false;
761        // If we can change mode for this scale gesture.
762        private boolean mCanChangeMode;
763        // If we have changed the film mode in this scaling gesture.
764        private boolean mModeChanged;
765        // If this scaling gesture should be ignored.
766        private boolean mIgnoreScalingGesture;
767        // whether the down action happened while the view is scrolling.
768        private boolean mDownInScrolling;
769        // If we should ignore all gestures other than onSingleTapUp.
770        private boolean mIgnoreSwipingGesture;
771
772        @Override
773        public boolean onSingleTapUp(float x, float y) {
774            // We do this in addition to onUp() because we want the snapback of
775            // setFilmMode to happen.
776            mHolding &= ~HOLD_TOUCH_DOWN;
777
778            if (mFilmMode && !mDownInScrolling) {
779                switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
780                setFilmMode(false);
781                mIgnoreUpEvent = true;
782                return true;
783            }
784
785            if (mListener != null) {
786                // Do the inverse transform of the touch coordinates.
787                Matrix m = getGLRoot().getCompensationMatrix();
788                Matrix inv = new Matrix();
789                m.invert(inv);
790                float[] pts = new float[] {x, y};
791                inv.mapPoints(pts);
792                mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
793            }
794            return true;
795        }
796
797        @Override
798        public boolean onDoubleTap(float x, float y) {
799            if (mIgnoreSwipingGesture) return true;
800            if (mPictures.get(0).isCamera()) return false;
801            PositionController controller = mPositionController;
802            float scale = controller.getImageScale();
803            // onDoubleTap happened on the second ACTION_DOWN.
804            // We need to ignore the next UP event.
805            mIgnoreUpEvent = true;
806            if (scale <= 1.0f || controller.isAtMinimalScale()) {
807                controller.zoomIn(x, y, Math.max(1.5f, scale * 1.5f));
808            } else {
809                controller.resetToFullView();
810            }
811            return true;
812        }
813
814        @Override
815        public boolean onScroll(float dx, float dy) {
816            if (mIgnoreSwipingGesture) return true;
817            mPositionController.startScroll(-dx, -dy);
818            return true;
819        }
820
821        @Override
822        public boolean onFling(float velocityX, float velocityY) {
823            if (mIgnoreSwipingGesture) return true;
824            if (swipeImages(velocityX, velocityY)) {
825                mIgnoreUpEvent = true;
826            } else if (mPositionController.fling(velocityX, velocityY)) {
827                mIgnoreUpEvent = true;
828            }
829            return true;
830        }
831
832        @Override
833        public boolean onScaleBegin(float focusX, float focusY) {
834            if (mIgnoreSwipingGesture) return true;
835            // We ignore the scaling gesture if it is a camera preview.
836            mIgnoreScalingGesture = mPictures.get(0).isCamera();
837            if (mIgnoreScalingGesture) {
838                return true;
839            }
840            mPositionController.beginScale(focusX, focusY);
841            // We can change mode if we are in film mode, or we are in page
842            // mode and at minimal scale.
843            mCanChangeMode = mFilmMode
844                    || mPositionController.isAtMinimalScale();
845            mModeChanged = false;
846            return true;
847        }
848
849        @Override
850        public boolean onScale(float focusX, float focusY, float scale) {
851            if (mIgnoreSwipingGesture) return true;
852            if (mIgnoreScalingGesture) return true;
853            if (mModeChanged) return true;
854            if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
855
856            // We wait for the scale change accumulated to a large enough change
857            // before reacting to it. Otherwise we may mistakenly treat a
858            // zoom-in gesture as zoom-out or vice versa.
859            if (scale > 0.99f && scale < 1.01f) return false;
860
861            int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
862
863            // If mode changes, we treat this scaling gesture has ended.
864            if (mCanChangeMode) {
865                if ((outOfRange < 0 && !mFilmMode) ||
866                        (outOfRange > 0 && mFilmMode)) {
867                    stopExtraScalingIfNeeded();
868
869                    // Removing the touch down flag allows snapback to happen
870                    // for film mode change.
871                    mHolding &= ~HOLD_TOUCH_DOWN;
872                    setFilmMode(!mFilmMode);
873
874                    // We need to call onScaleEnd() before setting mModeChanged
875                    // to true.
876                    onScaleEnd();
877                    mModeChanged = true;
878                    return true;
879                }
880           }
881
882            if (outOfRange != 0) {
883                startExtraScalingIfNeeded();
884            } else {
885                stopExtraScalingIfNeeded();
886            }
887            return true;
888        }
889
890        @Override
891        public void onScaleEnd() {
892            if (mIgnoreSwipingGesture) return;
893            if (mIgnoreScalingGesture) return;
894            if (mModeChanged) return;
895            mPositionController.endScale();
896        }
897
898        private void startExtraScalingIfNeeded() {
899            if (!mCancelExtraScalingPending) {
900                mHandler.sendEmptyMessageDelayed(
901                        MSG_CANCEL_EXTRA_SCALING, 700);
902                mPositionController.setExtraScalingRange(true);
903                mCancelExtraScalingPending = true;
904            }
905        }
906
907        private void stopExtraScalingIfNeeded() {
908            if (mCancelExtraScalingPending) {
909                mHandler.removeMessages(MSG_CANCEL_EXTRA_SCALING);
910                mPositionController.setExtraScalingRange(false);
911                mCancelExtraScalingPending = false;
912            }
913        }
914
915        @Override
916        public void onDown() {
917            if (mIgnoreSwipingGesture) return;
918
919            mHolding |= HOLD_TOUCH_DOWN;
920
921            if (mFilmMode && mPositionController.isScrolling()) {
922                mDownInScrolling = true;
923                mPositionController.stopScrolling();
924            } else {
925                mDownInScrolling = false;
926            }
927        }
928
929        @Override
930        public void onUp() {
931            if (mIgnoreSwipingGesture) return;
932
933            mHolding &= ~HOLD_TOUCH_DOWN;
934            mEdgeView.onRelease();
935
936            if (mIgnoreUpEvent) {
937                mIgnoreUpEvent = false;
938                return;
939            }
940
941            snapback();
942        }
943
944        public void setSwipingEnabled(boolean enabled) {
945            mIgnoreSwipingGesture = !enabled;
946        }
947    }
948
949    public void setSwipingEnabled(boolean enabled) {
950        mGestureListener.setSwipingEnabled(enabled);
951    }
952
953    private void setFilmMode(boolean enabled) {
954        if (mFilmMode == enabled) return;
955        mFilmMode = enabled;
956        mPositionController.setFilmMode(mFilmMode);
957        mModel.setNeedFullImage(!enabled);
958        mListener.onActionBarAllowed(!enabled);
959
960        // If we leave filmstrip mode, we should lock/unlock
961        if (!enabled) {
962            if (mPictures.get(0).isCamera()) {
963                mListener.lockOrientation();  // Transition C
964            } else {
965                mListener.unlockOrientation();  // Transition D
966            }
967        }
968    }
969
970    public boolean getFilmMode() {
971        return mFilmMode;
972    }
973
974    ////////////////////////////////////////////////////////////////////////////
975    //  Framework events
976    ////////////////////////////////////////////////////////////////////////////
977
978    public void pause() {
979        mPositionController.skipAnimation();
980        mTileView.freeTextures();
981        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
982            mPictures.get(i).setScreenNail(null);
983        }
984    }
985
986    public void resume() {
987        mTileView.prepareTextures();
988    }
989
990    // move to the camera preview and show controls after resume
991    public void resetToFirstPicture() {
992        mModel.moveTo(0);
993        setFilmMode(false);
994    }
995
996    ////////////////////////////////////////////////////////////////////////////
997    //  Rendering
998    ////////////////////////////////////////////////////////////////////////////
999
1000    @Override
1001    protected void render(GLCanvas canvas) {
1002        float filmRatio = mPositionController.getFilmRatio();
1003
1004        // Draw next photos. In page mode, we draw only one next photo.
1005        int lastPhoto = (filmRatio == 0f) ? 1 : SCREEN_NAIL_MAX;
1006        for (int i = lastPhoto; i > 0; i--) {
1007            Rect r = mPositionController.getPosition(i);
1008            mPictures.get(i).draw(canvas, r);
1009        }
1010
1011        // Draw current photo
1012        mPictures.get(0).draw(canvas, mPositionController.getPosition(0));
1013
1014        // Draw previous photos. In page mode, we draw only one previous photo.
1015        lastPhoto = (filmRatio == 0f) ? -1: -SCREEN_NAIL_MAX;
1016        for (int i = -1; i >= lastPhoto; i--) {
1017            Rect r = mPositionController.getPosition(i);
1018            mPictures.get(i).draw(canvas, r);
1019        }
1020
1021        mPositionController.advanceAnimation();
1022        checkFocusSwitching();
1023    }
1024
1025    ////////////////////////////////////////////////////////////////////////////
1026    //  Film mode focus switching
1027    ////////////////////////////////////////////////////////////////////////////
1028
1029    // Runs in GL thread.
1030    private void checkFocusSwitching() {
1031        if (!mFilmMode) return;
1032        if (mHandler.hasMessages(MSG_SWITCH_FOCUS)) return;
1033        if (switchPosition() != 0) {
1034            mHandler.sendEmptyMessage(MSG_SWITCH_FOCUS);
1035        }
1036    }
1037
1038    // Runs in main thread.
1039    private void switchFocus() {
1040        if (mHolding != 0) return;
1041        switch (switchPosition()) {
1042            case -1:
1043                switchToPrevImage();
1044                break;
1045            case 1:
1046                switchToNextImage();
1047                break;
1048        }
1049    }
1050
1051    // Returns -1 if we should switch focus to the previous picture, +1 if we
1052    // should switch to the next, 0 otherwise.
1053    private int switchPosition() {
1054        Rect curr = mPositionController.getPosition(0);
1055        int center = getWidth() / 2;
1056
1057        if (curr.left > center && mPrevBound < 0) {
1058            Rect prev = mPositionController.getPosition(-1);
1059            int currDist = curr.left - center;
1060            int prevDist = center - prev.right;
1061            if (prevDist < currDist) {
1062                return -1;
1063            }
1064        } else if (curr.right < center && mNextBound > 0) {
1065            Rect next = mPositionController.getPosition(1);
1066            int currDist = center - curr.right;
1067            int nextDist = next.left - center;
1068            if (nextDist < currDist) {
1069                return 1;
1070            }
1071        }
1072
1073        return 0;
1074    }
1075
1076    // Switch to the previous or next picture if the hit position is inside
1077    // one of their boxes. This runs in main thread.
1078    private void switchToHitPicture(int x, int y) {
1079        if (mPrevBound < 0) {
1080            Rect r = mPositionController.getPosition(-1);
1081            if (r.right >= x) {
1082                slideToPrevPicture();
1083                return;
1084            }
1085        }
1086
1087        if (mNextBound > 0) {
1088            Rect r = mPositionController.getPosition(1);
1089            if (r.left <= x) {
1090                slideToNextPicture();
1091                return;
1092            }
1093        }
1094    }
1095
1096    ////////////////////////////////////////////////////////////////////////////
1097    //  Page mode focus switching
1098    //
1099    //  We slide image to the next one or the previous one in two cases: 1: If
1100    //  the user did a fling gesture with enough velocity.  2 If the user has
1101    //  moved the picture a lot.
1102    ////////////////////////////////////////////////////////////////////////////
1103
1104    private boolean swipeImages(float velocityX, float velocityY) {
1105        if (mFilmMode) return false;
1106
1107        // Avoid swiping images if we're possibly flinging to view the
1108        // zoomed in picture vertically.
1109        PositionController controller = mPositionController;
1110        boolean isMinimal = controller.isAtMinimalScale();
1111        int edges = controller.getImageAtEdges();
1112        if (!isMinimal && Math.abs(velocityY) > Math.abs(velocityX))
1113            if ((edges & PositionController.IMAGE_AT_TOP_EDGE) == 0
1114                    || (edges & PositionController.IMAGE_AT_BOTTOM_EDGE) == 0)
1115                return false;
1116
1117        // If we are at the edge of the current photo and the sweeping velocity
1118        // exceeds the threshold, slide to the next / previous image.
1119        if (velocityX < -SWIPE_THRESHOLD && (isMinimal
1120                || (edges & PositionController.IMAGE_AT_RIGHT_EDGE) != 0)) {
1121            return slideToNextPicture();
1122        } else if (velocityX > SWIPE_THRESHOLD && (isMinimal
1123                || (edges & PositionController.IMAGE_AT_LEFT_EDGE) != 0)) {
1124            return slideToPrevPicture();
1125        }
1126
1127        return false;
1128    }
1129
1130    private void snapback() {
1131        if (mHolding != 0) return;
1132        if (!snapToNeighborImage()) {
1133            mPositionController.snapback();
1134        }
1135    }
1136
1137    private boolean snapToNeighborImage() {
1138        if (mFilmMode) return false;
1139
1140        Rect r = mPositionController.getPosition(0);
1141        int viewW = getWidth();
1142        int threshold = MOVE_THRESHOLD + gapToSide(r.width(), viewW);
1143
1144        // If we have moved the picture a lot, switching.
1145        if (viewW - r.right > threshold) {
1146            return slideToNextPicture();
1147        } else if (r.left > threshold) {
1148            return slideToPrevPicture();
1149        }
1150
1151        return false;
1152    }
1153
1154    private boolean slideToNextPicture() {
1155        if (mNextBound <= 0) return false;
1156        switchToNextImage();
1157        mPositionController.startHorizontalSlide();
1158        return true;
1159    }
1160
1161    private boolean slideToPrevPicture() {
1162        if (mPrevBound >= 0) return false;
1163        switchToPrevImage();
1164        mPositionController.startHorizontalSlide();
1165        return true;
1166    }
1167
1168    private static int gapToSide(int imageWidth, int viewWidth) {
1169        return Math.max(0, (viewWidth - imageWidth) / 2);
1170    }
1171
1172    ////////////////////////////////////////////////////////////////////////////
1173    //  Focus switching
1174    ////////////////////////////////////////////////////////////////////////////
1175
1176    private void switchToNextImage() {
1177        mModel.moveTo(mModel.getCurrentIndex() + 1);
1178    }
1179
1180    private void switchToPrevImage() {
1181        mModel.moveTo(mModel.getCurrentIndex() - 1);
1182    }
1183
1184    private void switchToFirstImage() {
1185        mModel.moveTo(0);
1186    }
1187
1188    ////////////////////////////////////////////////////////////////////////////
1189    //  Opening Animation
1190    ////////////////////////////////////////////////////////////////////////////
1191
1192    public void setOpenAnimationRect(Rect rect) {
1193        mPositionController.setOpenAnimationRect(rect);
1194    }
1195
1196    ////////////////////////////////////////////////////////////////////////////
1197    //  Capture Animation
1198    ////////////////////////////////////////////////////////////////////////////
1199
1200    public boolean switchWithCaptureAnimation(int offset) {
1201        GLRoot root = getGLRoot();
1202        root.lockRenderThread();
1203        try {
1204            return switchWithCaptureAnimationLocked(offset);
1205        } finally {
1206            root.unlockRenderThread();
1207        }
1208    }
1209
1210    private boolean switchWithCaptureAnimationLocked(int offset) {
1211        if (mHolding != 0) return true;
1212        if (offset == 1) {
1213            if (mNextBound <= 0) return false;
1214            // Temporary disable action bar until the capture animation is done.
1215            if (!mFilmMode) mListener.onActionBarAllowed(false);
1216            switchToNextImage();
1217            mPositionController.startCaptureAnimationSlide(-1);
1218        } else if (offset == -1) {
1219            if (mPrevBound >= 0) return false;
1220            switchToFirstImage();
1221            mPositionController.startCaptureAnimationSlide(1);
1222        } else {
1223            return false;
1224        }
1225        mHolding |= HOLD_CAPTURE_ANIMATION;
1226        Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
1227        mHandler.sendMessageDelayed(m, 800);
1228        return true;
1229    }
1230
1231    private void captureAnimationDone(int offset) {
1232        mHolding &= ~HOLD_CAPTURE_ANIMATION;
1233        if (offset == 1) {
1234            // move out of camera, unlock
1235            if (!mFilmMode) {
1236                // Now the capture animation is done, enable the action bar.
1237                mListener.onActionBarAllowed(true);
1238            }
1239        }
1240        snapback();
1241    }
1242
1243    ////////////////////////////////////////////////////////////////////////////
1244    //  Card deck effect calculation
1245    ////////////////////////////////////////////////////////////////////////////
1246
1247    // Returns the scrolling progress value for an object moving out of a
1248    // view. The progress value measures how much the object has moving out of
1249    // the view. The object currently displays in [left, right), and the view is
1250    // at [0, viewWidth].
1251    //
1252    // The returned value is negative when the object is moving right, and
1253    // positive when the object is moving left. The value goes to -1 or 1 when
1254    // the object just moves out of the view completely. The value is 0 if the
1255    // object currently fills the view.
1256    private static float calculateMoveOutProgress(int left, int right,
1257            int viewWidth) {
1258        // w = object width
1259        // viewWidth = view width
1260        int w = right - left;
1261
1262        // If the object width is smaller than the view width,
1263        //      |....view....|
1264        //                   |<-->|      progress = -1 when left = viewWidth
1265        //          |<-->|               progress = 0 when left = viewWidth / 2 - w / 2
1266        // |<-->|                        progress = 1 when left = -w
1267        if (w < viewWidth) {
1268            int zx = viewWidth / 2 - w / 2;
1269            if (left > zx) {
1270                return -(left - zx) / (float) (viewWidth - zx);  // progress = (0, -1]
1271            } else {
1272                return (left - zx) / (float) (-w - zx);  // progress = [0, 1]
1273            }
1274        }
1275
1276        // If the object width is larger than the view width,
1277        //             |..view..|
1278        //                      |<--------->| progress = -1 when left = viewWidth
1279        //             |<--------->|          progress = 0 between left = 0
1280        //          |<--------->|                          and right = viewWidth
1281        // |<--------->|                      progress = 1 when right = 0
1282        if (left > 0) {
1283            return -left / (float) viewWidth;
1284        }
1285
1286        if (right < viewWidth) {
1287            return (viewWidth - right) / (float) viewWidth;
1288        }
1289
1290        return 0;
1291    }
1292
1293    // Maps a scrolling progress value to the alpha factor in the fading
1294    // animation.
1295    private float getScrollAlpha(float scrollProgress) {
1296        return scrollProgress < 0 ? mAlphaInterpolator.getInterpolation(
1297                     1 - Math.abs(scrollProgress)) : 1.0f;
1298    }
1299
1300    // Maps a scrolling progress value to the scaling factor in the fading
1301    // animation.
1302    private float getScrollScale(float scrollProgress) {
1303        float interpolatedProgress = mScaleInterpolator.getInterpolation(
1304                Math.abs(scrollProgress));
1305        float scale = (1 - interpolatedProgress) +
1306                interpolatedProgress * TRANSITION_SCALE_FACTOR;
1307        return scale;
1308    }
1309
1310
1311    // This interpolator emulates the rate at which the perceived scale of an
1312    // object changes as its distance from a camera increases. When this
1313    // interpolator is applied to a scale animation on a view, it evokes the
1314    // sense that the object is shrinking due to moving away from the camera.
1315    private static class ZInterpolator {
1316        private float focalLength;
1317
1318        public ZInterpolator(float foc) {
1319            focalLength = foc;
1320        }
1321
1322        public float getInterpolation(float input) {
1323            return (1.0f - focalLength / (focalLength + input)) /
1324                (1.0f - focalLength / (focalLength + 1.0f));
1325        }
1326    }
1327
1328    // Returns an interpolated value for the page/film transition.
1329    // When ratio = 0, the result is from.
1330    // When ratio = 1, the result is to.
1331    private static float interpolate(float ratio, float from, float to) {
1332        return from + (to - from) * ratio * ratio;
1333    }
1334
1335    ////////////////////////////////////////////////////////////////////////////
1336    //  Simple public utilities
1337    ////////////////////////////////////////////////////////////////////////////
1338
1339    public void setListener(Listener listener) {
1340        mListener = listener;
1341    }
1342
1343    public Rect getPhotoRect(int index) {
1344        return mPositionController.getPosition(index);
1345    }
1346
1347
1348    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
1349        Rect location = new Rect();
1350        Utils.assertTrue(root.getBoundsOf(this, location));
1351
1352        Rect fullRect = bounds();
1353        PhotoFallbackEffect effect = new PhotoFallbackEffect();
1354        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
1355            MediaItem item = mModel.getMediaItem(i);
1356            if (item == null) continue;
1357            ScreenNail sc = mModel.getScreenNail(i);
1358            if (sc == null) continue;
1359            Rect rect = new Rect(getPhotoRect(i));
1360            if (!Rect.intersects(fullRect, rect)) continue;
1361            rect.offset(location.left, location.top);
1362
1363            RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true);
1364            canvas.beginRenderTarget(texture);
1365            sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight());
1366            canvas.endRenderTarget();
1367            effect.addEntry(item.getPath(), rect, texture);
1368        }
1369        return effect;
1370    }
1371}
1372