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