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