OSDN Git Service

Merge "Fade in/out the undo bar." into jb-dev
[android-x86/packages-apps-Gallery2.git] / src / com / android / gallery3d / ui / PhotoView.java
index adbf791..496d34d 100644 (file)
@@ -18,20 +18,24 @@ package com.android.gallery3d.ui;
 
 import android.content.Context;
 import android.graphics.Color;
+import android.graphics.Matrix;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Message;
+import android.util.FloatMath;
 import android.view.MotionEvent;
+import android.view.View.MeasureSpec;
 import android.view.animation.AccelerateInterpolator;
 
 import com.android.gallery3d.R;
 import com.android.gallery3d.app.GalleryActivity;
 import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.data.MediaItem;
 import com.android.gallery3d.data.MediaObject;
+import com.android.gallery3d.data.Path;
+import com.android.gallery3d.util.GalleryUtils;
 import com.android.gallery3d.util.RangeArray;
 
-import java.util.Arrays;
-
 public class PhotoView extends GLView {
     @SuppressWarnings("unused")
     private static final String TAG = "PhotoView";
@@ -54,6 +58,9 @@ public class PhotoView extends GLView {
         // not avaiable, width = height = 0.
         public void getImageSize(int offset, Size size);
 
+        // Returns the media item for the specified picture.
+        public MediaItem getMediaItem(int offset);
+
         // Returns the rotation for the specified picture.
         public int getImageRotation(int offset);
 
@@ -67,6 +74,38 @@ public class PhotoView extends GLView {
 
         // Returns true if the item is the Camera preview.
         public boolean isCamera(int offset);
+
+        // Returns true if the item is the Panorama.
+        public boolean isPanorama(int offset);
+
+        // Returns true if the item is a Video.
+        public boolean isVideo(int offset);
+
+        // Returns true if the item can be deleted.
+        public boolean isDeletable(int offset);
+
+        public static final int LOADING_INIT = 0;
+        public static final int LOADING_COMPLETE = 1;
+        public static final int LOADING_FAIL = 2;
+
+        public int getLoadingState(int offset);
+
+        // When data change happens, we need to decide which MediaItem to focus
+        // on.
+        //
+        // 1. If focus hint path != null, we try to focus on it if we can find
+        // it.  This is used for undo a deletion, so we can focus on the
+        // undeleted item.
+        //
+        // 2. Otherwise try to focus on the MediaItem that is currently focused,
+        // if we can find it.
+        //
+        // 3. Otherwise try to focus on the previous MediaItem or the next
+        // MediaItem, depending on the value of focus hint direction.
+        public static final int FOCUS_HINT_NEXT = 0;
+        public static final int FOCUS_HINT_PREVIOUS = 1;
+        public void setFocusHintDirection(int direction);
+        public void setFocusHintPath(Path path);
     }
 
     public interface Listener {
@@ -75,54 +114,49 @@ public class PhotoView extends GLView {
         public void unlockOrientation();
         public void onFullScreenChanged(boolean full);
         public void onActionBarAllowed(boolean allowed);
+        public void onActionBarWanted();
+        public void onCurrentImageUpdated();
+        public void onDeleteImage(Path path, int offset);
+        public void onUndoDeleteImage();
+        public void onCommitDeleteImage();
     }
 
-    // Here is a graph showing the places we need to lock/unlock device
-    // orientation:
+    // The rules about orientation locking:
     //
-    //           +------------+ A  +------------+
-    // Page mode |   Camera   |<---|   Photo    |
-    //           |  [locked]  |--->| [unlocked] |
-    //           +------------+  B +------------+
-    //                ^                  ^
-    //                | C                | D
-    //           +------------+    +------------+
-    //           |   Camera   |    |   Photo    |
-    // Film mode |    [*]     |    |    [*]     |
-    //           +------------+    +------------+
+    // (1) We need to lock the orientation if we are in page mode camera
+    // preview, so there is no (unwanted) rotation animation when the user
+    // rotates the device.
     //
-    // In Page mode, we want to lock in Camera because we don't want the system
-    // rotation animation. We also want to unlock in Photo because we want to
-    // show the system action bar in the right place.
+    // (2) We need to unlock the orientation if we want to show the action bar
+    // because the action bar follows the system orientation.
     //
-    // We don't show action bar in Film mode, so it's fine for it to be locked
-    // or unlocked in Film mode.
+    // The rules about action bar:
     //
-    // There are four transitions we need to check if we need to
-    // lock/unlock. Marked as A to D above and in the code.
-
-    private static final int MSG_SHOW_LOADING = 1;
+    // (1) If we are in film mode, we don't show action bar.
+    //
+    // (2) If we go from camera to gallery with capture animation, we show
+    // action bar.
     private static final int MSG_CANCEL_EXTRA_SCALING = 2;
     private static final int MSG_SWITCH_FOCUS = 3;
     private static final int MSG_CAPTURE_ANIMATION_DONE = 4;
-
-    private static final long DELAY_SHOW_LOADING = 250; // 250ms;
-
-    private static final int LOADING_INIT = 0;
-    private static final int LOADING_TIMEOUT = 1;
-    private static final int LOADING_COMPLETE = 2;
-    private static final int LOADING_FAIL = 3;
+    private static final int MSG_DELETE_ANIMATION_DONE = 5;
+    private static final int MSG_DELETE_DONE = 6;
+    private static final int MSG_HIDE_UNDO_BAR = 7;
 
     private static final int MOVE_THRESHOLD = 256;
     private static final float SWIPE_THRESHOLD = 300f;
 
     private static final float DEFAULT_TEXT_SIZE = 20;
     private static float TRANSITION_SCALE_FACTOR = 0.74f;
+    private static final int ICON_RATIO = 6;
 
     // whether we want to apply card deck effect in page mode.
     private static final boolean CARD_EFFECT = true;
 
-    // Used to calculate the scaling factor for the fading animation.
+    // whether we want to apply offset effect in film mode.
+    private static final boolean OFFSET_EFFECT = true;
+
+    // Used to calculate the scaling factor for the card deck effect.
     private ZInterpolator mScaleInterpolator = new ZInterpolator(0.5f);
 
     // Used to calculate the alpha factor for the fading animation.
@@ -132,14 +166,17 @@ public class PhotoView extends GLView {
     // We keep this many previous ScreenNails. (also this many next ScreenNails)
     public static final int SCREEN_NAIL_MAX = 3;
 
+    // These are constants for the delete gesture.
+    private static final int SWIPE_ESCAPE_VELOCITY = 500; // dp/sec
+    private static final int MAX_DISMISS_VELOCITY = 2000; // dp/sec
+
     // The picture entries, the valid index is from -SCREEN_NAIL_MAX to
     // SCREEN_NAIL_MAX.
     private final RangeArray<Picture> mPictures =
             new RangeArray<Picture>(-SCREEN_NAIL_MAX, SCREEN_NAIL_MAX);
+    private Size[] mSizes = new Size[2 * SCREEN_NAIL_MAX + 1];
 
-    private final long mDataVersion[] = new long[2 * SCREEN_NAIL_MAX + 1];
-    private final int mFromIndex[] = new int[2 * SCREEN_NAIL_MAX + 1];
-
+    private final MyGestureListener mGestureListener;
     private final GestureRecognizer mGestureRecognizer;
     private final PositionController mPositionController;
 
@@ -149,22 +186,18 @@ public class PhotoView extends GLView {
     private StringTexture mNoThumbnailText;
     private TileImageView mTileView;
     private EdgeView mEdgeView;
+    private UndoBarView mUndoBar;
     private Texture mVideoPlayIcon;
 
-    private boolean mShowVideoPlayIcon;
-    private ProgressSpinner mLoadingSpinner;
-
     private SynchronizedHandler mHandler;
 
-    private int mLoadingState = LOADING_COMPLETE;
-
     private Point mImageCenter = new Point();
     private boolean mCancelExtraScalingPending;
     private boolean mFilmMode = false;
     private int mDisplayRotation = 0;
     private int mCompensation = 0;
-    private boolean mFullScreen = true;
-    private Rect mCameraNaturalFrame = new Rect();
+    private boolean mFullScreenCamera;
+    private Rect mCameraRelativeFrame = new Rect();
     private Rect mCameraRect = new Rect();
 
     // [mPrevBound, mNextBound] is the range of index for all pictures in the
@@ -180,6 +213,19 @@ public class PhotoView extends GLView {
     private int mHolding;
     private static final int HOLD_TOUCH_DOWN = 1;
     private static final int HOLD_CAPTURE_ANIMATION = 2;
+    private static final int HOLD_DELETE = 4;
+
+    // mTouchBoxIndex is the index of the box that is touched by the down
+    // gesture in film mode. The value Integer.MAX_VALUE means no box was
+    // touched.
+    private int mTouchBoxIndex = Integer.MAX_VALUE;
+    // Whether the box indicated by mTouchBoxIndex is deletable. Only meaningful
+    // if mTouchBoxIndex is not Integer.MAX_VALUE.
+    private boolean mTouchBoxDeletable;
+    // This is the index of the last deleted item. This is only used as a hint
+    // to hide the undo button when we are too far away from the deleted
+    // item. The value Integer.MAX_VALUE means there is no such hint.
+    private int mUndoIndexHint = Integer.MAX_VALUE;
 
     public PhotoView(GalleryActivity activity) {
         mTileView = new TileImageView(activity);
@@ -187,7 +233,15 @@ public class PhotoView extends GLView {
         Context context = activity.getAndroidContext();
         mEdgeView = new EdgeView(context);
         addComponent(mEdgeView);
-        mLoadingSpinner = new ProgressSpinner(context);
+        mUndoBar = new UndoBarView(context);
+        addComponent(mUndoBar);
+        mUndoBar.setVisibility(GLView.INVISIBLE);
+        mUndoBar.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(GLView v) {
+                    mListener.onUndoDeleteImage();
+                }
+            });
         mLoadingText = StringTexture.newInstance(
                 context.getString(R.string.loading),
                 DEFAULT_TEXT_SIZE, Color.WHITE);
@@ -197,16 +251,19 @@ public class PhotoView extends GLView {
 
         mHandler = new MyHandler(activity.getGLRoot());
 
-        mGestureRecognizer = new GestureRecognizer(
-                context, new MyGestureListener());
+        mGestureListener = new MyGestureListener();
+        mGestureRecognizer = new GestureRecognizer(context, mGestureListener);
 
         mPositionController = new PositionController(context,
                 new PositionController.Listener() {
                     public void invalidate() {
                         PhotoView.this.invalidate();
                     }
-                    public boolean isHolding() {
-                        return mHolding != 0;
+                    public boolean isHoldingDown() {
+                        return (mHolding & HOLD_TOUCH_DOWN) != 0;
+                    }
+                    public boolean isHoldingDelete() {
+                        return (mHolding & HOLD_DELETE) != 0;
                     }
                     public void onPull(int offset, int direction) {
                         mEdgeView.onPull(offset, direction);
@@ -219,7 +276,6 @@ public class PhotoView extends GLView {
                     }
                 });
         mVideoPlayIcon = new ResourceTexture(context, R.drawable.ic_control_play);
-        Arrays.fill(mDataVersion, INVALID_DATA_VERSION);
         for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
             if (i == 0) {
                 mPictures.put(i, new FullPicture());
@@ -242,17 +298,6 @@ public class PhotoView extends GLView {
         @Override
         public void handleMessage(Message message) {
             switch (message.what) {
-                case MSG_SHOW_LOADING: {
-                    if (mLoadingState == LOADING_INIT) {
-                        // We don't need the opening animation
-                        mPositionController.setOpenAnimationRect(null);
-
-                        mLoadingSpinner.startAnimation();
-                        mLoadingState = LOADING_TIMEOUT;
-                        invalidate();
-                    }
-                    break;
-                }
                 case MSG_CANCEL_EXTRA_SCALING: {
                     mGestureRecognizer.cancelScale();
                     mPositionController.setExtraScalingRange(false);
@@ -269,147 +314,191 @@ public class PhotoView extends GLView {
                     captureAnimationDone(message.arg1);
                     break;
                 }
+                case MSG_DELETE_ANIMATION_DONE: {
+                    // message.obj is the Path of the MediaItem which should be
+                    // deleted. message.arg1 is the offset of the image.
+                    mListener.onDeleteImage((Path) message.obj, message.arg1);
+                    // Normally a box which finishes delete animation will hold
+                    // position until the underlying MediaItem is actually
+                    // deleted, and HOLD_DELETE will be cancelled that time. In
+                    // case the MediaItem didn't actually get deleted in 2
+                    // seconds, we will cancel HOLD_DELETE and make it bounce
+                    // back.
+
+                    // We make sure there is at most one MSG_DELETE_DONE
+                    // in the handler.
+                    mHandler.removeMessages(MSG_DELETE_DONE);
+                    Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+                    mHandler.sendMessageDelayed(m, 2000);
+                    break;
+                }
+                case MSG_DELETE_DONE: {
+                    if (!mHandler.hasMessages(MSG_DELETE_ANIMATION_DONE)) {
+                        mHolding &= ~HOLD_DELETE;
+                        snapback();
+                    }
+                    break;
+                }
+                case MSG_HIDE_UNDO_BAR: {
+                    checkHideUndoBar(UNDO_BAR_TIMEOUT);
+                    break;
+                }
                 default: throw new AssertionError(message.what);
             }
         }
     };
 
-    private void updateLoadingState() {
-        // Possible transitions of mLoadingState:
-        //        INIT --> TIMEOUT, COMPLETE, FAIL
-        //     TIMEOUT --> COMPLETE, FAIL, INIT
-        //    COMPLETE --> INIT
-        //        FAIL --> INIT
-        if (mModel.getLevelCount() != 0 || mModel.getScreenNail() != null) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_COMPLETE;
-        } else if (mModel.isFailedToLoad()) {
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mLoadingState = LOADING_FAIL;
-            // We don't want the opening animation after loading failure
-            mPositionController.setOpenAnimationRect(null);
-        } else if (mLoadingState != LOADING_INIT) {
-            mLoadingState = LOADING_INIT;
-            mHandler.removeMessages(MSG_SHOW_LOADING);
-            mHandler.sendEmptyMessageDelayed(
-                    MSG_SHOW_LOADING, DELAY_SHOW_LOADING);
-        }
-    }
-
     ////////////////////////////////////////////////////////////////////////////
     //  Data/Image change notifications
     ////////////////////////////////////////////////////////////////////////////
 
-    public void notifyDataChange(long[] versions, int prevBound, int nextBound) {
+    public void notifyDataChange(int[] fromIndex, int prevBound, int nextBound) {
         mPrevBound = prevBound;
         mNextBound = nextBound;
 
-        // Check if the data version actually changed.
-        boolean changed = false;
-        int N = 2 * SCREEN_NAIL_MAX + 1;
-        for (int i = 0; i < N; i++) {
-            if (versions[i] != mDataVersion[i]) {
-                changed = true;
-                break;
+        // Update mTouchBoxIndex
+        if (mTouchBoxIndex != Integer.MAX_VALUE) {
+            int k = mTouchBoxIndex;
+            mTouchBoxIndex = Integer.MAX_VALUE;
+            for (int i = 0; i < 2 * SCREEN_NAIL_MAX + 1; i++) {
+                if (fromIndex[i] == k) {
+                    mTouchBoxIndex = i - SCREEN_NAIL_MAX;
+                    break;
+                }
             }
         }
-        if (!changed) return;
 
-        // Create the mFromIndex array, which records the index where the picture
-        // come from. The value Integer.MAX_VALUE means it's a new picture.
-        for (int i = 0; i < N; i++) {
-            long v = versions[i];
-            if (v == INVALID_DATA_VERSION) {
-                mFromIndex[i] = Integer.MAX_VALUE;
-                continue;
+        // Hide undo button if we are too far away
+        if (mUndoIndexHint != Integer.MAX_VALUE) {
+            if (Math.abs(mUndoIndexHint - mModel.getCurrentIndex()) >= 3) {
+                hideUndoBar();
             }
-
-            // Try to find the same version number in the old array
-            int j;
-            for (j = 0; j < N; j++) {
-                if (mDataVersion[j] == v) {
-                    break;
-                }
-            }
-            mFromIndex[i] = (j < N) ? j - SCREEN_NAIL_MAX : Integer.MAX_VALUE;
         }
 
-        // Copy the new data version
-        for (int i = 0; i < N; i++) {
-            mDataVersion[i] = versions[i];
+        // Update the ScreenNails.
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+            Picture p =  mPictures.get(i);
+            p.reload();
+            mSizes[i + SCREEN_NAIL_MAX] = p.getSize();
         }
 
+        boolean wasDeleting = mPositionController.hasDeletingBox();
+
         // Move the boxes
-        mPositionController.moveBox(mFromIndex, mPrevBound < 0, mNextBound > 0,
-                mModel.isCamera(0));
+        mPositionController.moveBox(fromIndex, mPrevBound < 0, mNextBound > 0,
+                mModel.isCamera(0), mSizes);
 
-        // Update the ScreenNails.
         for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
-            mPictures.get(i).reload();
+            setPictureSize(i);
+        }
+
+        boolean isDeleting = mPositionController.hasDeletingBox();
+
+        // If the deletion is done, make HOLD_DELETE persist for only the time
+        // needed for a snapback animation.
+        if (wasDeleting && !isDeleting) {
+            mHandler.removeMessages(MSG_DELETE_DONE);
+            Message m = mHandler.obtainMessage(MSG_DELETE_DONE);
+            mHandler.sendMessageDelayed(
+                    m, PositionController.SNAPBACK_ANIMATION_TIME);
         }
 
         invalidate();
     }
 
+    public boolean isDeleting() {
+        return (mHolding & HOLD_DELETE) != 0
+                && mPositionController.hasDeletingBox();
+    }
+
     public void notifyImageChange(int index) {
+        if (index == 0) {
+            mListener.onCurrentImageUpdated();
+        }
         mPictures.get(index).reload();
+        setPictureSize(index);
         invalidate();
     }
 
-    @Override
-    protected void onOrient(int displayRotation, int compensation) {
-        // onLayout will be called soon. We need to change the size and rotation
-        // of the Camera ScreenNail, but we don't want it start moving because
-        // the view size will be changed soon.
-        mDisplayRotation = displayRotation;
-        mCompensation = compensation;
-        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
-            Picture p = mPictures.get(i);
-            if (p.isCamera()) {
-                p.updateSize(true);
-            }
-        }
+    private void setPictureSize(int index) {
+        Picture p = mPictures.get(index);
+        mPositionController.setImageSize(index, p.getSize(),
+                index == 0 && p.isCamera() ? mCameraRect : null);
     }
 
     @Override
     protected void onLayout(
             boolean changeSize, int left, int top, int right, int bottom) {
-        mTileView.layout(left, top, right, bottom);
-        mEdgeView.layout(left, top, right, bottom);
-        updateConstrainedFrame();
+        int w = right - left;
+        int h = bottom - top;
+        mTileView.layout(0, 0, w, h);
+        mEdgeView.layout(0, 0, w, h);
+        mUndoBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        mUndoBar.layout(0, h - mUndoBar.getMeasuredHeight(), w, h);
+
+        GLRoot root = getGLRoot();
+        int displayRotation = root.getDisplayRotation();
+        int compensation = root.getCompensation();
+        if (mDisplayRotation != displayRotation
+                || mCompensation != compensation) {
+            mDisplayRotation = displayRotation;
+            mCompensation = compensation;
+
+            // We need to change the size and rotation of the Camera ScreenNail,
+            // but we don't want it to animate because the size doen't actually
+            // change in the eye of the user.
+            for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; i++) {
+                Picture p = mPictures.get(i);
+                if (p.isCamera()) {
+                    p.forceSize();
+                }
+            }
+        }
+
+        updateCameraRect();
+        mPositionController.setConstrainedFrame(mCameraRect);
         if (changeSize) {
             mPositionController.setViewSize(getWidth(), getHeight());
         }
     }
 
-    // Update the constrained frame due to layout change.
-    private void updateConstrainedFrame() {
+    // Update the camera rectangle due to layout change or camera relative frame
+    // change.
+    private void updateCameraRect() {
+        // Get the width and height in framework orientation because the given
+        // mCameraRelativeFrame is in that coordinates.
         int w = getWidth();
         int h = getHeight();
-        int rotation = getCameraRotation();
-        if (rotation % 180 != 0) {
+        if (mCompensation % 180 != 0) {
             int tmp = w;
             w = h;
             h = tmp;
         }
+        int l = mCameraRelativeFrame.left;
+        int t = mCameraRelativeFrame.top;
+        int r = mCameraRelativeFrame.right;
+        int b = mCameraRelativeFrame.bottom;
 
-        int l = mCameraNaturalFrame.left;
-        int t = mCameraNaturalFrame.top;
-        int r = mCameraNaturalFrame.right;
-        int b = mCameraNaturalFrame.bottom;
-
-        switch (rotation) {
+        // Now convert it to the coordinates we are using.
+        switch (mCompensation) {
             case 0: mCameraRect.set(l, t, r, b); break;
             case 90: mCameraRect.set(h - b, l, h - t, r); break;
             case 180: mCameraRect.set(w - r, h - b, w - l, h - t); break;
             case 270: mCameraRect.set(t, w - r, b, w - l); break;
         }
 
-        mPositionController.setConstrainedFrame(mCameraRect);
+        Log.d(TAG, "compensation = " + mCompensation
+                + ", CameraRelativeFrame = " + mCameraRelativeFrame
+                + ", mCameraRect = " + mCameraRect);
     }
 
-    public void setCameraNaturalFrame(Rect frame) {
-        mCameraNaturalFrame.set(frame);
+    public void setCameraRelativeFrame(Rect frame) {
+        mCameraRelativeFrame.set(frame);
+        updateCameraRect();
+        // Originally we do
+        //     mPositionController.setConstrainedFrame(mCameraRect);
+        // here, but it is moved to a parameter of the setImageSize() call, so
+        // it can be updated atomically with the CameraScreenNail's size change.
     }
 
     // Returns the rotation we need to do to the camera texture before drawing
@@ -419,6 +508,10 @@ public class PhotoView extends GLView {
         return (mCompensation - mDisplayRotation + 360) % 360;
     }
 
+    private int getPanoramaRotation() {
+        return mCompensation;
+    }
+
     ////////////////////////////////////////////////////////////////////////////
     //  Pictures
     ////////////////////////////////////////////////////////////////////////////
@@ -428,14 +521,20 @@ public class PhotoView extends GLView {
         void draw(GLCanvas canvas, Rect r);
         void setScreenNail(ScreenNail s);
         boolean isCamera();  // whether the picture is a camera preview
-        void updateSize(boolean force);  // called when mCompensation changes
+        boolean isDeletable();  // whether the picture can be deleted
+        void forceSize();  // called when mCompensation changes
+        Size getSize();
     };
 
     class FullPicture implements Picture {
         private int mRotation;
         private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsVideo;
+        private boolean mIsDeletable;
+        private int mLoadingState = Model.LOADING_INIT;
+        private Size mSize = new Size();
         private boolean mWasCameraCenter;
-
         public void FullPicture(TileImageView tileView) {
             mTileView = tileView;
         }
@@ -444,17 +543,31 @@ public class PhotoView extends GLView {
         public void reload() {
             // mImageWidth and mImageHeight will get updated
             mTileView.notifyModelInvalidated();
-            mTileView.setAlpha(1.0f);
 
             mIsCamera = mModel.isCamera(0);
+            mIsPanorama = mModel.isPanorama(0);
+            mIsVideo = mModel.isVideo(0);
+            mIsDeletable = mModel.isDeletable(0);
+            mLoadingState = mModel.getLoadingState(0);
             setScreenNail(mModel.getScreenNail(0));
-            updateSize(false);
-            updateLoadingState();
+            updateSize();
+        }
+
+        @Override
+        public Size getSize() {
+            return mSize;
         }
 
         @Override
-        public void updateSize(boolean force) {
-            if (mIsCamera) {
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(0, mSize);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
                 mRotation = getCameraRotation();
             } else {
                 mRotation = mModel.getImageRotation(0);
@@ -462,30 +575,13 @@ public class PhotoView extends GLView {
 
             int w = mTileView.mImageWidth;
             int h = mTileView.mImageHeight;
-            mPositionController.setImageSize(0,
-                    getRotated(mRotation, w, h),
-                    getRotated(mRotation, h, w),
-                    force);
+            mSize.width = getRotated(mRotation, w, h);
+            mSize.height = getRotated(mRotation, h, w);
         }
 
         @Override
         public void draw(GLCanvas canvas, Rect r) {
-            boolean isCenter = mPositionController.isCenter();
-
-            if (mLoadingState == LOADING_COMPLETE) {
-                setTileViewPosition(r);
-                PhotoView.super.render(canvas);
-            }
-            renderMessage(canvas, r.centerX(), r.centerY());
-
-            if (mIsCamera) {
-                boolean full = !mFilmMode && isCenter
-                        && mPositionController.isAtMinimalScale();
-                if (full != mFullScreen) {
-                    mFullScreen = full;
-                    mListener.onFullScreenChanged(full);
-                }
-            }
+            drawTileView(canvas, r);
 
             // We want to have the following transitions:
             // (1) Move camera preview out of its place: switch to film mode
@@ -496,6 +592,7 @@ public class PhotoView extends GLView {
             // Holdings except touch-down prevent the transitions.
             if ((mHolding & ~HOLD_TOUCH_DOWN) != 0) return;
 
+            boolean isCenter = mPositionController.isCenter();
             boolean isCameraCenter = mIsCamera && isCenter;
 
             if (mWasCameraCenter && mIsCamera && !isCenter && !mFilmMode) {
@@ -505,14 +602,9 @@ public class PhotoView extends GLView {
                 setFilmMode(false);
             }
 
-            if (isCenter && !mFilmMode) {
-                if (mIsCamera) {
-                    // move into camera, lock
-                    mListener.lockOrientation();  // Transition A
-                } else {
-                    // move out of camera, unlock
-                    mListener.unlockOrientation();  // Transition B
-                }
+            if (isCameraCenter && !mFilmMode) {
+                // Move into camera in page mode, lock
+                mListener.lockOrientation();
             }
 
             mWasCameraCenter = isCameraCenter;
@@ -528,22 +620,26 @@ public class PhotoView extends GLView {
             return mIsCamera;
         }
 
-        private void setTileViewPosition(Rect r) {
-            TileImageView t = mTileView;
+        @Override
+        public boolean isDeletable() {
+            return mIsDeletable;
+        }
 
-            // Find out the bitmap coordinates of the center of the view
-            int imageW = mPositionController.getImageWidth();
-            int imageH = mPositionController.getImageHeight();
-            float scale = mPositionController.getImageScale();
+        private void drawTileView(GLCanvas canvas, Rect r) {
+            float imageScale = mPositionController.getImageScale();
             int viewW = getWidth();
             int viewH = getHeight();
-            int centerX = (int) (imageW / 2f +
-                    (viewW / 2f - r.exactCenterX()) / scale + 0.5f);
-            int centerY = (int) (imageH / 2f +
-                    (viewH / 2f - r.exactCenterY()) / scale + 0.5f);
-
-            boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
-                    && !mIsCamera && !mPictures.get(-1).isCamera();
+            float cx = r.exactCenterX();
+            float cy = r.exactCenterY();
+            float scale = 1f;  // the scaling factor due to card effect
+
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && !mIsCamera
+                    && filmRatio != 1f && !mPictures.get(-1).isCamera()
+                    && !mPositionController.inOpeningAnimation();
+            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+                    && filmRatio == 1f && r.centerY() != viewH / 2;
             if (wantsCardEffect) {
                 // Calculate the move-out progress value.
                 int left = r.left;
@@ -554,67 +650,73 @@ public class PhotoView extends GLView {
                 // We only want to apply the fading animation if the scrolling
                 // movement is to the right.
                 if (progress < 0) {
-                    if (right - left < viewW) {
+                    scale = getScrollScale(progress);
+                    float alpha = getScrollAlpha(progress);
+                    scale = interpolate(filmRatio, scale, 1f);
+                    alpha = interpolate(filmRatio, alpha, 1f);
+
+                    imageScale *= scale;
+                    canvas.multiplyAlpha(alpha);
+
+                    float cxPage;  // the cx value in page mode
+                    if (right - left <= viewW) {
                         // If the picture is narrower than the view, keep it at
                         // the center of the view.
-                        centerX = imageW / 2;
+                        cxPage = viewW / 2f;
                     } else {
                         // If the picture is wider than the view (it's
                         // zoomed-in), keep the left edge of the object align
                         // the the left edge of the view.
-                        centerX = Math.round(viewW / 2f / scale);
+                        cxPage = (right - left) * scale / 2f;
                     }
-                    scale *= getScrollScale(progress);
-                    t.setAlpha(getScrollAlpha(progress));
+                    cx = interpolate(filmRatio, cxPage, cx);
                 }
+            } else if (wantsOffsetEffect) {
+                float offset = (float) (r.centerY() - viewH / 2) / viewH;
+                float alpha = getOffsetAlpha(offset);
+                canvas.multiplyAlpha(alpha);
             }
 
-            // set the position of the tile view
-            int inverseX = imageW - centerX;
-            int inverseY = imageH - centerY;
-            int rotation = mRotation;
-            switch (rotation) {
-                case 0: t.setPosition(centerX, centerY, scale, 0); break;
-                case 90: t.setPosition(centerY, inverseX, scale, 90); break;
-                case 180: t.setPosition(inverseX, inverseY, scale, 180); break;
-                case 270: t.setPosition(inverseY, centerX, scale, 270); break;
-                default:
-                    throw new IllegalArgumentException(String.valueOf(rotation));
-            }
-        }
+            // Draw the tile view.
+            setTileViewPosition(cx, cy, viewW, viewH, imageScale);
+            renderChild(canvas, mTileView);
 
-        private void renderMessage(GLCanvas canvas, int x, int y) {
-            // Draw the progress spinner and the text below it
-            //
-            // (x, y) is where we put the center of the spinner.
-            // s is the size of the video play icon, and we use s to layout text
-            // because we want to keep the text at the same place when the video
-            // play icon is shown instead of the spinner.
-            int w = getWidth();
-            int h = getHeight();
-            int s = Math.min(w, h) / 6;
-
-            if (mLoadingState == LOADING_TIMEOUT) {
-                StringTexture m = mLoadingText;
-                ProgressSpinner p = mLoadingSpinner;
-                p.draw(canvas, x - p.getWidth() / 2, y - p.getHeight() / 2);
-                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
-                invalidate(); // we need to keep the spinner rotating
-            } else if (mLoadingState == LOADING_FAIL) {
-                StringTexture m = mNoThumbnailText;
-                m.draw(canvas, x - m.getWidth() / 2, y + s / 2 + 5);
+            // Draw the play video icon and the message.
+            canvas.translate((int) (cx + 0.5f), (int) (cy + 0.5f));
+            int s = (int) (scale * Math.min(r.width(), r.height()) + 0.5f);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
             }
 
             // Draw a debug indicator showing which picture has focus (index ==
             // 0).
-            // canvas.fillRect(x - 10, y - 10, 20, 20, 0x80FF00FF);
+            //canvas.fillRect(-10, -10, 20, 20, 0x80FF00FF);
+
+            canvas.restore();
+        }
+
+        // Set the position of the tile view
+        private void setTileViewPosition(float cx, float cy,
+                int viewW, int viewH, float scale) {
+            // Find out the bitmap coordinates of the center of the view
+            int imageW = mPositionController.getImageWidth();
+            int imageH = mPositionController.getImageHeight();
+            int centerX = (int) (imageW / 2f + (viewW / 2f - cx) / scale + 0.5f);
+            int centerY = (int) (imageH / 2f + (viewH / 2f - cy) / scale + 0.5f);
 
-            // Draw the video play icon (in the place where the spinner was)
-            if (mShowVideoPlayIcon
-                    && mLoadingState != LOADING_INIT
-                    && mLoadingState != LOADING_TIMEOUT) {
-                mVideoPlayIcon.draw(canvas, x - s / 2, y - s / 2, s, s);
+            int inverseX = imageW - centerX;
+            int inverseY = imageH - centerY;
+            int x, y;
+            switch (mRotation) {
+                case 0: x = centerX; y = centerY; break;
+                case 90: x = centerY; y = inverseX; break;
+                case 180: x = inverseX; y = inverseY; break;
+                case 270: x = inverseY; y = centerX; break;
+                default:
+                    throw new RuntimeException(String.valueOf(mRotation));
             }
+            mTileView.setPosition(x, y, scale, mRotation);
         }
     }
 
@@ -622,8 +724,12 @@ public class PhotoView extends GLView {
         private int mIndex;
         private int mRotation;
         private ScreenNail mScreenNail;
-        private Size mSize = new Size();
         private boolean mIsCamera;
+        private boolean mIsPanorama;
+        private boolean mIsVideo;
+        private boolean mIsDeletable;
+        private int mLoadingState = Model.LOADING_INIT;
+        private Size mSize = new Size();
 
         public ScreenNailPicture(int index) {
             mIndex = index;
@@ -632,98 +738,145 @@ public class PhotoView extends GLView {
         @Override
         public void reload() {
             mIsCamera = mModel.isCamera(mIndex);
+            mIsPanorama = mModel.isPanorama(mIndex);
+            mIsVideo = mModel.isVideo(mIndex);
+            mIsDeletable = mModel.isDeletable(mIndex);
+            mLoadingState = mModel.getLoadingState(mIndex);
             setScreenNail(mModel.getScreenNail(mIndex));
+            updateSize();
+        }
+
+        @Override
+        public Size getSize() {
+            return mSize;
         }
 
         @Override
         public void draw(GLCanvas canvas, Rect r) {
             if (mScreenNail == null) {
-                // Draw a placeholder rectange if there will be a picture in
-                // this position.
+                // Draw a placeholder rectange if there should be a picture in
+                // this position (but somehow there isn't).
                 if (mIndex >= mPrevBound && mIndex <= mNextBound) {
-                    canvas.fillRect(r.left, r.top, r.width(), r.height(),
-                            PLACEHOLDER_COLOR);
+                    drawPlaceHolder(canvas, r);
                 }
                 return;
             }
-            if (r.left >= getWidth() || r.right <= 0 ||
-                    r.top >= getHeight() || r.bottom <= 0) {
+            int w = getWidth();
+            int h = getHeight();
+            if (r.left >= w || r.right <= 0 || r.top >= h || r.bottom <= 0) {
                 mScreenNail.noDraw();
                 return;
             }
 
-            if (mIsCamera && mFullScreen != false) {
-                mFullScreen = false;
-                mListener.onFullScreenChanged(false);
-            }
-
-            boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
-                && (mIndex > 0) && !mPictures.get(0).isCamera();
-
-            int w = getWidth();
-            int drawW = getRotated(mRotation, r.width(), r.height());
-            int drawH = getRotated(mRotation, r.height(), r.width());
-            int cx = wantsCardEffect ? w / 2 : r.centerX();
+            float filmRatio = mPositionController.getFilmRatio();
+            boolean wantsCardEffect = CARD_EFFECT && mIndex > 0
+                    && filmRatio != 1f && !mPictures.get(0).isCamera();
+            boolean wantsOffsetEffect = OFFSET_EFFECT && mIsDeletable
+                    && filmRatio == 1f && r.centerY() != h / 2;
+            int cx = wantsCardEffect
+                    ? (int) (interpolate(filmRatio, w / 2, r.centerX()) + 0.5f)
+                    : r.centerX();
             int cy = r.centerY();
-            int flags = GLCanvas.SAVE_FLAG_MATRIX;
-
-            if (wantsCardEffect) flags |= GLCanvas.SAVE_FLAG_ALPHA;
-            canvas.save(flags);
+            canvas.save(GLCanvas.SAVE_FLAG_MATRIX | GLCanvas.SAVE_FLAG_ALPHA);
             canvas.translate(cx, cy);
             if (wantsCardEffect) {
                 float progress = (float) (w / 2 - r.centerX()) / w;
                 progress = Utils.clamp(progress, -1, 1);
                 float alpha = getScrollAlpha(progress);
                 float scale = getScrollScale(progress);
+                alpha = interpolate(filmRatio, alpha, 1f);
+                scale = interpolate(filmRatio, scale, 1f);
                 canvas.multiplyAlpha(alpha);
                 canvas.scale(scale, scale, 1);
+            } else if (wantsOffsetEffect) {
+                float offset = (float) (r.centerY() - h / 2) / h;
+                float alpha = getOffsetAlpha(offset);
+                canvas.multiplyAlpha(alpha);
             }
             if (mRotation != 0) {
                 canvas.rotate(mRotation, 0, 0, 1);
             }
+            int drawW = getRotated(mRotation, r.width(), r.height());
+            int drawH = getRotated(mRotation, r.height(), r.width());
             mScreenNail.draw(canvas, -drawW / 2, -drawH / 2, drawW, drawH);
+            if (isScreenNailAnimating()) {
+                invalidate();
+            }
+            int s = Math.min(drawW, drawH);
+            if (mIsVideo) drawVideoPlayIcon(canvas, s);
+            if (mLoadingState == Model.LOADING_FAIL) {
+                drawLoadingFailMessage(canvas);
+            }
             canvas.restore();
         }
 
+        private boolean isScreenNailAnimating() {
+            return (mScreenNail instanceof BitmapScreenNail)
+                    && ((BitmapScreenNail) mScreenNail).isAnimating();
+        }
+
         @Override
         public void setScreenNail(ScreenNail s) {
-            if (mScreenNail == s) return;
             mScreenNail = s;
-            updateSize(false);
         }
 
         @Override
-        public void updateSize(boolean force) {
-            if (mIsCamera) {
+        public void forceSize() {
+            updateSize();
+            mPositionController.forceImageSize(mIndex, mSize);
+        }
+
+        private void updateSize() {
+            if (mIsPanorama) {
+                mRotation = getPanoramaRotation();
+            } else if (mIsCamera) {
                 mRotation = getCameraRotation();
             } else {
                 mRotation = mModel.getImageRotation(mIndex);
             }
 
-            int w = 0, h = 0;
             if (mScreenNail != null) {
-                w = mScreenNail.getWidth();
-                h = mScreenNail.getHeight();
-            } else if (mModel != null) {
+                mSize.width = mScreenNail.getWidth();
+                mSize.height = mScreenNail.getHeight();
+            } else {
                 // If we don't have ScreenNail available, we can still try to
                 // get the size information of it.
                 mModel.getImageSize(mIndex, mSize);
-                w = mSize.width;
-                h = mSize.height;
             }
 
-            if (w != 0 && h != 0)  {
-                mPositionController.setImageSize(mIndex,
-                        getRotated(mRotation, w, h),
-                        getRotated(mRotation, h, w),
-                        force);
-            }
+            int w = mSize.width;
+            int h = mSize.height;
+            mSize.width = getRotated(mRotation, w, h);
+            mSize.height = getRotated(mRotation, h, w);
         }
 
         @Override
         public boolean isCamera() {
             return mIsCamera;
         }
+
+        @Override
+        public boolean isDeletable() {
+            return mIsDeletable;
+        }
+    }
+
+    // Draw a gray placeholder in the specified rectangle.
+    private void drawPlaceHolder(GLCanvas canvas, Rect r) {
+        canvas.fillRect(r.left, r.top, r.width(), r.height(), PLACEHOLDER_COLOR);
+    }
+
+    // Draw the video play icon (in the place where the spinner was)
+    private void drawVideoPlayIcon(GLCanvas canvas, int side) {
+        int s = side / ICON_RATIO;
+        // Draw the video play icon at the center
+        mVideoPlayIcon.draw(canvas, -s / 2, -s / 2, s, s);
+    }
+
+    // Draw the "no thumbnail" message
+    private void drawLoadingFailMessage(GLCanvas canvas) {
+        StringTexture m = mNoThumbnailText;
+        m.draw(canvas, -m.getWidth() / 2, -m.getHeight() / 2);
     }
 
     private static int getRotated(int degree, int original, int theother) {
@@ -748,22 +901,51 @@ public class PhotoView extends GLView {
         private boolean mModeChanged;
         // If this scaling gesture should be ignored.
         private boolean mIgnoreScalingGesture;
+        // If we have seen a scaling gesture.
+        private boolean mSeenScaling;
+        // whether the down action happened while the view is scrolling.
+        private boolean mDownInScrolling;
+        // If we should ignore all gestures other than onSingleTapUp.
+        private boolean mIgnoreSwipingGesture;
+        // If a scrolling has happened after a down gesture.
+        private boolean mScrolledAfterDown;
+        // If the first scrolling move is in X direction. In the film mode, X
+        // direction scrolling is normal scrolling. but Y direction scrolling is
+        // a delete gesture.
+        private boolean mFirstScrollX;
+        // The accumulated Y delta that has been sent to mPositionController.
+        private int mDeltaY;
+        // The accumulated scaling change from a scaling gesture.
+        private float mAccScale;
 
         @Override
         public boolean onSingleTapUp(float x, float y) {
-            if (mFilmMode) {
+            // We do this in addition to onUp() because we want the snapback of
+            // setFilmMode to happen.
+            mHolding &= ~HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && !mDownInScrolling) {
+                switchToHitPicture((int) (x + 0.5f), (int) (y + 0.5f));
                 setFilmMode(false);
+                mIgnoreUpEvent = true;
                 return true;
             }
 
             if (mListener != null) {
-                mListener.onSingleTapUp((int) x, (int) y);
+                // Do the inverse transform of the touch coordinates.
+                Matrix m = getGLRoot().getCompensationMatrix();
+                Matrix inv = new Matrix();
+                m.invert(inv);
+                float[] pts = new float[] {x, y};
+                inv.mapPoints(pts);
+                mListener.onSingleTapUp((int) (pts[0] + 0.5f), (int) (pts[1] + 0.5f));
             }
             return true;
         }
 
         @Override
         public boolean onDoubleTap(float x, float y) {
+            if (mIgnoreSwipingGesture) return true;
             if (mPictures.get(0).isCamera()) return false;
             PositionController controller = mPositionController;
             float scale = controller.getImageScale();
@@ -779,23 +961,114 @@ public class PhotoView extends GLView {
         }
 
         @Override
-        public boolean onScroll(float dx, float dy) {
-            mPositionController.startScroll(-dx, -dy);
+        public boolean onScroll(float dx, float dy, float totalX, float totalY) {
+            if (mIgnoreSwipingGesture) return true;
+            if (!mScrolledAfterDown) {
+                mScrolledAfterDown = true;
+                mFirstScrollX = (Math.abs(dx) > Math.abs(dy));
+            }
+
+            int dxi = (int) (-dx + 0.5f);
+            int dyi = (int) (-dy + 0.5f);
+            if (mFilmMode) {
+                if (mFirstScrollX) {
+                    mPositionController.scrollFilmX(dxi);
+                } else {
+                    if (mTouchBoxIndex == Integer.MAX_VALUE) return true;
+                    int newDeltaY = calculateDeltaY(totalY);
+                    int d = newDeltaY - mDeltaY;
+                    if (d != 0) {
+                        mPositionController.scrollFilmY(mTouchBoxIndex, d);
+                        mDeltaY = newDeltaY;
+                    }
+                }
+            } else {
+                mPositionController.scrollPage(dxi, dyi);
+            }
             return true;
         }
 
+        private int calculateDeltaY(float delta) {
+            if (mTouchBoxDeletable) return (int) (delta + 0.5f);
+
+            // don't let items that can't be deleted be dragged more than
+            // maxScrollDistance, and make it harder and harder to drag.
+            int size = getHeight();
+            float maxScrollDistance = 0.15f * size;
+            if (Math.abs(delta) >= size) {
+                delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+            } else {
+                delta = maxScrollDistance *
+                        FloatMath.sin((delta / size) * (float) (Math.PI / 2));
+            }
+            return (int) (delta + 0.5f);
+        }
+
         @Override
         public boolean onFling(float velocityX, float velocityY) {
+            if (mIgnoreSwipingGesture) return true;
+            if (mSeenScaling) return true;
             if (swipeImages(velocityX, velocityY)) {
                 mIgnoreUpEvent = true;
-            } else if (mPositionController.fling(velocityX, velocityY)) {
-                mIgnoreUpEvent = true;
+            } else {
+                flingImages(velocityX, velocityY);
             }
             return true;
         }
 
+        private boolean flingImages(float velocityX, float velocityY) {
+            int vx = (int) (velocityX + 0.5f);
+            int vy = (int) (velocityY + 0.5f);
+            if (!mFilmMode) {
+                return mPositionController.flingPage(vx, vy);
+            }
+            if (Math.abs(velocityX) > Math.abs(velocityY)) {
+                return mPositionController.flingFilmX(vx);
+            }
+            // If we scrolled in Y direction fast enough, treat it as a delete
+            // gesture.
+            if (!mFilmMode || mTouchBoxIndex == Integer.MAX_VALUE
+                    || !mTouchBoxDeletable) {
+                return false;
+            }
+            int maxVelocity = (int) GalleryUtils.dpToPixel(MAX_DISMISS_VELOCITY);
+            int escapeVelocity =
+                    (int) GalleryUtils.dpToPixel(SWIPE_ESCAPE_VELOCITY);
+            int centerY = mPositionController.getPosition(mTouchBoxIndex)
+                    .centerY();
+            boolean fastEnough = (Math.abs(vy) > escapeVelocity)
+                    && (Math.abs(vy) > Math.abs(vx))
+                    && ((vy > 0) == (centerY > getHeight() / 2));
+            if (fastEnough) {
+                vy = Math.min(vy, maxVelocity);
+                int duration = mPositionController.flingFilmY(mTouchBoxIndex, vy);
+                if (duration >= 0) {
+                    mPositionController.setPopFromTop(vy < 0);
+                    deleteAfterAnimation(duration);
+                    // We reset mTouchBoxIndex, so up() won't check if Y
+                    // scrolled far enough to be a delete gesture.
+                    mTouchBoxIndex = Integer.MAX_VALUE;
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private void deleteAfterAnimation(int duration) {
+            MediaItem item = mModel.getMediaItem(mTouchBoxIndex);
+            if (item == null) return;
+            mListener.onCommitDeleteImage();
+            mUndoIndexHint = mModel.getCurrentIndex() + mTouchBoxIndex;
+            mHolding |= HOLD_DELETE;
+            Message m = mHandler.obtainMessage(MSG_DELETE_ANIMATION_DONE);
+            m.obj = item.getPath();
+            m.arg1 = mTouchBoxIndex;
+            mHandler.sendMessageDelayed(m, duration);
+        }
+
         @Override
         public boolean onScaleBegin(float focusX, float focusY) {
+            if (mIgnoreSwipingGesture) return true;
             // We ignore the scaling gesture if it is a camera preview.
             mIgnoreScalingGesture = mPictures.get(0).isCamera();
             if (mIgnoreScalingGesture) {
@@ -807,26 +1080,28 @@ public class PhotoView extends GLView {
             mCanChangeMode = mFilmMode
                     || mPositionController.isAtMinimalScale();
             mModeChanged = false;
+            mSeenScaling = true;
+            mAccScale = 1f;
             return true;
         }
 
         @Override
         public boolean onScale(float focusX, float focusY, float scale) {
-            if (mIgnoreScalingGesture) {
-                return true;
-            }
+            if (mIgnoreSwipingGesture) return true;
+            if (mIgnoreScalingGesture) return true;
             if (mModeChanged) return true;
             if (Float.isNaN(scale) || Float.isInfinite(scale)) return false;
 
-            // We wait for the scale change accumulated to a large enough change
-            // before reacting to it. Otherwise we may mistakenly treat a
-            // zoom-in gesture as zoom-out or vice versa.
-            if (scale > 0.99f && scale < 1.01f) return false;
-
             int outOfRange = mPositionController.scaleBy(scale, focusX, focusY);
 
+            // We wait for a large enough scale change before changing mode.
+            // Otherwise we may mistakenly treat a zoom-in gesture as zoom-out
+            // or vice versa.
+            mAccScale *= scale;
+            boolean largeEnough = (mAccScale < 0.97f || mAccScale > 1.03f);
+
             // If mode changes, we treat this scaling gesture has ended.
-            if (mCanChangeMode) {
+            if (mCanChangeMode && largeEnough) {
                 if ((outOfRange < 0 && !mFilmMode) ||
                         (outOfRange > 0 && mFilmMode)) {
                     stopExtraScalingIfNeeded();
@@ -854,9 +1129,8 @@ public class PhotoView extends GLView {
 
         @Override
         public void onScaleEnd() {
-            if (mIgnoreScalingGesture) {
-                return;
-            }
+            if (mIgnoreSwipingGesture) return;
+            if (mIgnoreScalingGesture) return;
             if (mModeChanged) return;
             mPositionController.endScale();
         }
@@ -879,15 +1153,62 @@ public class PhotoView extends GLView {
         }
 
         @Override
-        public void onDown() {
+        public void onDown(float x, float y) {
+            checkHideUndoBar(UNDO_BAR_TOUCHED);
+
+            mDeltaY = 0;
+            mSeenScaling = false;
+
+            if (mIgnoreSwipingGesture) return;
+
             mHolding |= HOLD_TOUCH_DOWN;
+
+            if (mFilmMode && mPositionController.isScrolling()) {
+                mDownInScrolling = true;
+                mPositionController.stopScrolling();
+            } else {
+                mDownInScrolling = false;
+            }
+
+            mScrolledAfterDown = false;
+            if (mFilmMode) {
+                int xi = (int) (x + 0.5f);
+                int yi = (int) (y + 0.5f);
+                mTouchBoxIndex = mPositionController.hitTest(xi, yi);
+                if (mTouchBoxIndex < mPrevBound || mTouchBoxIndex > mNextBound) {
+                    mTouchBoxIndex = Integer.MAX_VALUE;
+                } else {
+                    mTouchBoxDeletable =
+                            mPictures.get(mTouchBoxIndex).isDeletable();
+                }
+            } else {
+                mTouchBoxIndex = Integer.MAX_VALUE;
+            }
         }
 
         @Override
         public void onUp() {
+            if (mIgnoreSwipingGesture) return;
+
             mHolding &= ~HOLD_TOUCH_DOWN;
             mEdgeView.onRelease();
 
+            // If we scrolled in Y direction far enough, treat it as a delete
+            // gesture.
+            if (mFilmMode && mScrolledAfterDown && !mFirstScrollX
+                    && mTouchBoxIndex != Integer.MAX_VALUE) {
+                Rect r = mPositionController.getPosition(mTouchBoxIndex);
+                int h = getHeight();
+                if (Math.abs(r.centerY() - h * 0.5f) > 0.4f * h) {
+                    int duration = mPositionController
+                            .flingFilmY(mTouchBoxIndex, 0);
+                    if (duration >= 0) {
+                        mPositionController.setPopFromTop(r.centerY() < h * 0.5f);
+                        deleteAfterAnimation(duration);
+                    }
+                }
+            }
+
             if (mIgnoreUpEvent) {
                 mIgnoreUpEvent = false;
                 return;
@@ -895,6 +1216,14 @@ public class PhotoView extends GLView {
 
             snapback();
         }
+
+        public void setSwipingEnabled(boolean enabled) {
+            mIgnoreSwipingGesture = !enabled;
+        }
+    }
+
+    public void setSwipingEnabled(boolean enabled) {
+        mGestureListener.setSwipingEnabled(enabled);
     }
 
     private void setFilmMode(boolean enabled) {
@@ -902,15 +1231,13 @@ public class PhotoView extends GLView {
         mFilmMode = enabled;
         mPositionController.setFilmMode(mFilmMode);
         mModel.setNeedFullImage(!enabled);
+        mModel.setFocusHintDirection(
+                mFilmMode ? Model.FOCUS_HINT_PREVIOUS : Model.FOCUS_HINT_NEXT);
         mListener.onActionBarAllowed(!enabled);
 
-        // If we leave filmstrip mode, we should lock/unlock
-        if (!enabled) {
-            if (mPictures.get(0).isCamera()) {
-                mListener.lockOrientation();  // Transition C
-            } else {
-                mListener.unlockOrientation();  // Transition D
-            }
+        // Move into camera in page mode, lock
+        if (!enabled && mPictures.get(0).isCamera()) {
+            mListener.lockOrientation();
         }
     }
 
@@ -941,30 +1268,82 @@ public class PhotoView extends GLView {
     }
 
     ////////////////////////////////////////////////////////////////////////////
+    //  Undo Bar
+    ////////////////////////////////////////////////////////////////////////////
+
+    private int mUndoBarState;
+    private static final int UNDO_BAR_SHOW = 1;
+    private static final int UNDO_BAR_TIMEOUT = 2;
+    private static final int UNDO_BAR_TOUCHED = 4;
+
+    public void showUndoBar() {
+        mHandler.removeMessages(MSG_HIDE_UNDO_BAR);
+        mUndoBarState = UNDO_BAR_SHOW;
+        mUndoBar.animateVisibility(GLView.VISIBLE);
+        mHandler.sendEmptyMessageDelayed(MSG_HIDE_UNDO_BAR, 3000);
+    }
+
+    public void hideUndoBar() {
+        mHandler.removeMessages(MSG_HIDE_UNDO_BAR);
+        mListener.onCommitDeleteImage();
+        mUndoBar.animateVisibility(GLView.INVISIBLE);
+        mUndoBarState = 0;
+        mUndoIndexHint = Integer.MAX_VALUE;
+    }
+
+    // Check if the all conditions for hiding the undo bar have been met. The
+    // conditions are: it has been three seconds since last showing, and the
+    // user has touched.
+    private void checkHideUndoBar(int addition) {
+        mUndoBarState |= addition;
+        if (mUndoBarState ==
+                (UNDO_BAR_SHOW | UNDO_BAR_TIMEOUT | UNDO_BAR_TOUCHED)) {
+            hideUndoBar();
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
     //  Rendering
     ////////////////////////////////////////////////////////////////////////////
 
     @Override
     protected void render(GLCanvas canvas) {
-        // Draw next photos
-        for (int i = 1; i <= SCREEN_NAIL_MAX; i++) {
-            Rect r = mPositionController.getPosition(i);
-            mPictures.get(i).draw(canvas, r);
-            // In page mode, we draw only one next photo.
-            if (!mFilmMode) break;
+        // Check if the camera preview occupies the full screen.
+        boolean full = !mFilmMode && mPictures.get(0).isCamera()
+                && mPositionController.isCenter()
+                && mPositionController.isAtMinimalScale();
+        if (full != mFullScreenCamera) {
+            mFullScreenCamera = full;
+            mListener.onFullScreenChanged(full);
         }
 
-        // Draw current photo
-        mPictures.get(0).draw(canvas, mPositionController.getPosition(0));
+        // Determine how many photos we need to draw in addition to the center
+        // one.
+        int neighbors;
+        if (mFullScreenCamera) {
+            neighbors = 0;
+        } else {
+            // In page mode, we draw only one previous/next photo. But if we are
+            // doing capture animation, we want to draw all photos.
+            boolean inPageMode = (mPositionController.getFilmRatio() == 0f);
+            boolean inCaptureAnimation =
+                    ((mHolding & HOLD_CAPTURE_ANIMATION) != 0);
+            if (inPageMode && !inCaptureAnimation) {
+                neighbors = 1;
+            } else {
+                neighbors = SCREEN_NAIL_MAX;
+            }
+        }
 
-        // Draw previous photos
-        for (int i = -1; i >= -SCREEN_NAIL_MAX; i--) {
+        // Draw photos from back to front
+        for (int i = neighbors; i >= -neighbors; i--) {
             Rect r = mPositionController.getPosition(i);
             mPictures.get(i).draw(canvas, r);
-            // In page mode, we draw only one previous photo.
-            if (!mFilmMode) break;
         }
 
+        renderChild(canvas, mEdgeView);
+        renderChild(canvas, mUndoBar);
+
         mPositionController.advanceAnimation();
         checkFocusSwitching();
     }
@@ -1020,6 +1399,26 @@ public class PhotoView extends GLView {
         return 0;
     }
 
+    // Switch to the previous or next picture if the hit position is inside
+    // one of their boxes. This runs in main thread.
+    private void switchToHitPicture(int x, int y) {
+        if (mPrevBound < 0) {
+            Rect r = mPositionController.getPosition(-1);
+            if (r.right >= x) {
+                slideToPrevPicture();
+                return;
+            }
+        }
+
+        if (mNextBound > 0) {
+            Rect r = mPositionController.getPosition(1);
+            if (r.left <= x) {
+                slideToNextPicture();
+                return;
+            }
+        }
+    }
+
     ////////////////////////////////////////////////////////////////////////////
     //  Page mode focus switching
     //
@@ -1055,7 +1454,7 @@ public class PhotoView extends GLView {
     }
 
     private void snapback() {
-        if (mHolding != 0) return;
+        if ((mHolding & ~HOLD_DELETE) != 0) return;
         if (!snapToNeighborImage()) {
             mPositionController.snapback();
         }
@@ -1144,6 +1543,17 @@ public class PhotoView extends GLView {
             mPositionController.startCaptureAnimationSlide(-1);
         } else if (offset == -1) {
             if (mPrevBound >= 0) return false;
+            if (mFilmMode) setFilmMode(false);
+
+            // If we are too far away from the first image (so that we don't
+            // have all the ScreenNails in-between), we go directly without
+            // animation.
+            if (mModel.getCurrentIndex() > SCREEN_NAIL_MAX) {
+                switchToFirstImage();
+                mPositionController.skipToFinalPosition();
+                return true;
+            }
+
             switchToFirstImage();
             mPositionController.startCaptureAnimationSlide(1);
         } else {
@@ -1151,18 +1561,16 @@ public class PhotoView extends GLView {
         }
         mHolding |= HOLD_CAPTURE_ANIMATION;
         Message m = mHandler.obtainMessage(MSG_CAPTURE_ANIMATION_DONE, offset, 0);
-        mHandler.sendMessageDelayed(m, 800);
+        mHandler.sendMessageDelayed(m, PositionController.CAPTURE_ANIMATION_TIME);
         return true;
     }
 
     private void captureAnimationDone(int offset) {
         mHolding &= ~HOLD_CAPTURE_ANIMATION;
-        if (offset == 1) {
-            // move out of camera, unlock
-            if (!mFilmMode) {
-                // Now the capture animation is done, enable the action bar.
-                mListener.onActionBarAllowed(true);
-            }
+        if (offset == 1 && !mFilmMode) {
+            // Now the capture animation is done, enable the action bar.
+            mListener.onActionBarAllowed(true);
+            mListener.onActionBarWanted();
         }
         snapback();
     }
@@ -1252,6 +1660,21 @@ public class PhotoView extends GLView {
         }
     }
 
+    // Returns an interpolated value for the page/film transition.
+    // When ratio = 0, the result is from.
+    // When ratio = 1, the result is to.
+    private static float interpolate(float ratio, float from, float to) {
+        return from + (to - from) * ratio * ratio;
+    }
+
+    // Returns the alpha factor in film mode if a picture is not in the center.
+    // The 0.03 lower bound is to make the item always visible a bit.
+    private float getOffsetAlpha(float offset) {
+        offset /= 0.5f;
+        float alpha = (offset > 0) ? (1 - offset) : (1 + offset);
+        return Utils.clamp(alpha, 0.03f, 1f);
+    }
+
     ////////////////////////////////////////////////////////////////////////////
     //  Simple public utilities
     ////////////////////////////////////////////////////////////////////////////
@@ -1260,7 +1683,34 @@ public class PhotoView extends GLView {
         mListener = listener;
     }
 
-    public void showVideoPlayIcon(boolean show) {
-        mShowVideoPlayIcon = show;
+    public Rect getPhotoRect(int index) {
+        return mPositionController.getPosition(index);
+    }
+
+    public PhotoFallbackEffect buildFallbackEffect(GLView root, GLCanvas canvas) {
+        Rect location = new Rect();
+        Utils.assertTrue(root.getBoundsOf(this, location));
+
+        Rect fullRect = bounds();
+        PhotoFallbackEffect effect = new PhotoFallbackEffect();
+        for (int i = -SCREEN_NAIL_MAX; i <= SCREEN_NAIL_MAX; ++i) {
+            MediaItem item = mModel.getMediaItem(i);
+            if (item == null) continue;
+            ScreenNail sc = mModel.getScreenNail(i);
+            if (!(sc instanceof BitmapScreenNail)
+                    || ((BitmapScreenNail) sc).isShowingPlaceholder()) continue;
+
+            // Now, sc is BitmapScreenNail and is not showing placeholder
+            Rect rect = new Rect(getPhotoRect(i));
+            if (!Rect.intersects(fullRect, rect)) continue;
+            rect.offset(location.left, location.top);
+
+            RawTexture texture = new RawTexture(sc.getWidth(), sc.getHeight(), true);
+            canvas.beginRenderTarget(texture);
+            sc.draw(canvas, 0, 0, sc.getWidth(), sc.getHeight());
+            canvas.endRenderTarget();
+            effect.addEntry(item.getPath(), rect, texture);
+        }
+        return effect;
     }
 }