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