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