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