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