OSDN Git Service

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