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