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