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