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