OSDN Git Service

Add capture animation in Gallery.
authorChih-Chung Chang <chihchung@google.com>
Fri, 20 Apr 2012 12:06:19 +0000 (20:06 +0800)
committerChih-Chung Chang <chihchung@google.com>
Tue, 24 Apr 2012 10:51:07 +0000 (18:51 +0800)
Change-Id: Ibf95cc64f37a4518377e64124af6606c4f14cdaa

src/com/android/gallery3d/app/AbstractGalleryActivity.java
src/com/android/gallery3d/app/Gallery.java
src/com/android/gallery3d/app/PhotoPage.java
src/com/android/gallery3d/app/PickerActivity.java
src/com/android/gallery3d/ui/CaptureAnimation.java [new file with mode: 0644]
src/com/android/gallery3d/ui/PhotoView.java
src/com/android/gallery3d/ui/PositionController.java

index 0b9461d..09e72c0 100644 (file)
@@ -206,6 +206,18 @@ public class AbstractGalleryActivity extends Activity implements GalleryActivity
     }
 
     @Override
+    public void onBackPressed() {
+        // send the back event to the top sub-state
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            getStateManager().onBackPressed();
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    @Override
     public GalleryActionBar getGalleryActionBar() {
         if (mActionBar == null) {
             mActionBar = new GalleryActionBar(this);
index 60eb069..c8fbd53 100644 (file)
@@ -233,18 +233,6 @@ public final class Gallery extends AbstractGalleryActivity implements OnCancelLi
     }
 
     @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
     protected void onResume() {
         Utils.assertTrue(getStateManager().getStateCount() > 0);
         super.onResume();
index 6be302b..6617ce6 100644 (file)
@@ -403,11 +403,18 @@ public class PhotoPage extends ActivityState
     protected void onBackPressed() {
         if (mShowDetails) {
             hideDetails();
-        } else {
+        } else if (!switchWithCaptureAnimation(-1)) {
             super.onBackPressed();
         }
     }
 
+    // Switch to the previous or next picture using the capture animation.
+    // The offset is -1 to switch to the previous picture, 1 to switch to
+    // the next picture.
+    public boolean switchWithCaptureAnimation(int offset) {
+        return mPhotoView.switchWithCaptureAnimation(offset);
+    }
+
     @Override
     protected boolean onCreateActionBar(Menu menu) {
         MenuInflater inflater = ((Activity) mActivity).getMenuInflater();
index d63e237..f5b2cbd 100644 (file)
@@ -78,18 +78,6 @@ public class PickerActivity extends AbstractGalleryActivity
     }
 
     @Override
-    public void onBackPressed() {
-        // send the back event to the top sub-state
-        GLRoot root = getGLRoot();
-        root.lockRenderThread();
-        try {
-            getStateManager().getTopState().onBackPressed();
-        } finally {
-            root.unlockRenderThread();
-        }
-    }
-
-    @Override
     public void onClick(View v) {
         if (v.getId() == R.id.cancel) finish();
     }
diff --git a/src/com/android/gallery3d/ui/CaptureAnimation.java b/src/com/android/gallery3d/ui/CaptureAnimation.java
new file mode 100644 (file)
index 0000000..87c054a
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.ui;
+
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+public class CaptureAnimation {
+    // The amount of change for zooming out.
+    private static final float ZOOM_DELTA = 0.2f;
+    // Pre-calculated value for convenience.
+    private static final float ZOOM_IN_BEGIN = 1f - ZOOM_DELTA;
+
+    private static final Interpolator sZoomOutInterpolator =
+            new DecelerateInterpolator();
+    private static final Interpolator sZoomInInterpolator =
+            new AccelerateInterpolator();
+    private static final Interpolator sSlideInterpolator =
+        new AccelerateDecelerateInterpolator();
+
+    // Calculate the slide factor based on the give time fraction.
+    public static float calculateSlide(float fraction) {
+        return sSlideInterpolator.getInterpolation(fraction);
+    }
+
+    // Calculate the scale factor based on the given time fraction.
+    public static float calculateScale(float fraction) {
+        float value;
+        if (fraction <= 0.5f) {
+            // Zoom in for the beginning.
+            value = 1f - ZOOM_DELTA *
+                    sZoomOutInterpolator.getInterpolation(fraction * 2);
+        } else {
+            // Zoom out for the ending.
+            value = ZOOM_IN_BEGIN + ZOOM_DELTA *
+                    sZoomInInterpolator.getInterpolation((fraction - 0.5f) * 2f);
+        }
+        return value;
+    }
+}
index 109a5d9..66941be 100644 (file)
@@ -76,6 +76,7 @@ public class PhotoView extends GLView {
     private static final int MSG_SHOW_LOADING = 1;
     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;
 
@@ -89,6 +90,8 @@ public class PhotoView extends GLView {
 
     private static final float DEFAULT_TEXT_SIZE = 20;
     private static float TRANSITION_SCALE_FACTOR = 0.74f;
+
+    // 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.
@@ -140,6 +143,16 @@ public class PhotoView extends GLView {
     private int mPrevBound;
     private int mNextBound;
 
+    // This variable prevents us doing snapback until its values goes to 0. This
+    // happens if the user gesture is still in progress or we are in a capture
+    // animation.
+    // HOLD_TOUCH_DOWN_FROM_CAMERA is an extra flag set together with
+    // HOLD_TOUCH_DOWN if the touch down starts from camera preview.
+    private int mHolding;
+    private static final int HOLD_TOUCH_DOWN = 1;
+    private static final int HOLD_TOUCH_DOWN_FROM_CAMERA = 2;
+    private static final int HOLD_CAPTURE_ANIMATION = 4;
+
     public PhotoView(GalleryActivity activity) {
         mTileView = new TileImageView(activity);
         addComponent(mTileView);
@@ -164,8 +177,14 @@ public class PhotoView extends GLView {
                     public void invalidate() {
                         PhotoView.this.invalidate();
                     }
-                    public boolean isDown() {
-                        return mGestureRecognizer.isDown();
+                    public boolean isHolding() {
+                        // We want the film mode change happen as soon as
+                        // possible even if the touch is still down.
+                        if ((mHolding & HOLD_TOUCH_DOWN_FROM_CAMERA) != 0) {
+                            return false;
+                        } else {
+                            return mHolding != 0;
+                        }
                     }
                     public void onPull(int offset, int direction) {
                         mEdgeView.onPull(offset, direction);
@@ -222,6 +241,10 @@ public class PhotoView extends GLView {
                     switchFocus();
                     break;
                 }
+                case MSG_CAPTURE_ANIMATION_DONE: {
+                    captureAnimationDone();
+                    break;
+                }
                 default: throw new AssertionError(message.what);
             }
         }
@@ -316,14 +339,17 @@ public class PhotoView extends GLView {
         void reload();
         void draw(GLCanvas canvas, Rect r);
         void setScreenNail(ScreenNail s);
-        boolean isEnabled();
+        boolean isCamera();  // whether the picture is a camera preview
     };
 
+    private boolean isCameraScreenNail(ScreenNail s) {
+        return s != null && !(s instanceof BitmapScreenNail);
+    }
+
     class FullPicture implements Picture {
         private int mRotation;
-
-        // This is a temporary hack to switch mode when entering/leaving camera.
-        private volatile boolean mIsNonBitmap;
+        private boolean mIsCamera;
+        private boolean mWasCenter;
 
         public void FullPicture(TileImageView tileView) {
             mTileView = tileView;
@@ -333,7 +359,7 @@ public class PhotoView extends GLView {
         public void reload() {
             // mImageWidth and mImageHeight will get updated
             mTileView.notifyModelInvalidated();
-            if (CARD_EFFECT) mTileView.setAlpha(1.0f);
+            mTileView.setAlpha(1.0f);
 
             mRotation = mModel.getImageRotation(0);
             int w = mTileView.mImageWidth;
@@ -355,22 +381,31 @@ public class PhotoView extends GLView {
             renderMessage(canvas, r.centerX(), r.centerY());
 
             boolean isCenter = r.centerX() == getWidth() / 2;
-            if (mIsNonBitmap && !isCenter && !mFilmMode) {
-                setFilmMode(true);
-            } else if (mIsNonBitmap && isCenter && mFilmMode) {
-                setFilmMode(false);
+
+            // We want to have following transitions:
+            // (1) Move camera preview out of its place: switch to film mode
+            // (2) Move camera preview into its place: switch to page mode
+            // The extra mWasCenter check makes sure (1) does not apply if in
+            // page mode, we move _to_ the camera preview from another picture.
+            if ((mHolding & ~(HOLD_TOUCH_DOWN | HOLD_TOUCH_DOWN_FROM_CAMERA)) == 0) {
+                if (mWasCenter && !isCenter && mIsCamera && !mFilmMode) {
+                    setFilmMode(true);
+                } else if (mIsCamera && isCenter && mFilmMode) {
+                    setFilmMode(false);
+                }
             }
+            mWasCenter = isCenter;
         }
 
         @Override
         public void setScreenNail(ScreenNail s) {
-            mIsNonBitmap = (s != null && !(s instanceof BitmapScreenNail));
+            mIsCamera = isCameraScreenNail(s);
             mTileView.setScreenNail(s);
         }
 
         @Override
-        public boolean isEnabled() {
-            return true;
+        public boolean isCamera() {
+            return mIsCamera;
         }
 
         private void setTileViewPosition(Rect r) {
@@ -387,7 +422,9 @@ public class PhotoView extends GLView {
             int centerY = (int) (imageH / 2f +
                     (viewH / 2f - r.exactCenterY()) / scale + 0.5f);
 
-            if (CARD_EFFECT && !mFilmMode) {
+            boolean wantsCardEffect = CARD_EFFECT && !mFilmMode
+                && !mPictures.get(-1).isCamera();
+            if (wantsCardEffect) {
                 // Calculate the move-out progress value.
                 int left = r.left;
                 int right = r.right;
@@ -463,10 +500,10 @@ public class PhotoView extends GLView {
 
     private class ScreenNailPicture implements Picture {
         private int mIndex;
-        private boolean mEnabled;
         private int mRotation;
         private ScreenNail mScreenNail;
         private Size mSize = new Size();
+        private boolean mIsCamera;
 
         public ScreenNailPicture(int index) {
             mIndex = index;
@@ -494,20 +531,20 @@ public class PhotoView extends GLView {
                 return;
             }
 
-            boolean applyFadingAnimation =
-                CARD_EFFECT && mIndex > 0 && !mFilmMode;
+            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 = applyFadingAnimation ? w / 2 : r.centerX();
+            int cx = wantsCardEffect ? w / 2 : r.centerX();
             int cy = r.centerY();
             int flags = GLCanvas.SAVE_FLAG_MATRIX;
 
-            if (applyFadingAnimation) flags |= GLCanvas.SAVE_FLAG_ALPHA;
+            if (wantsCardEffect) flags |= GLCanvas.SAVE_FLAG_ALPHA;
             canvas.save(flags);
             canvas.translate(cx, cy);
-            if (applyFadingAnimation) {
+            if (wantsCardEffect) {
                 float progress = (float) (w / 2 - r.centerX()) / w;
                 progress = Utils.clamp(progress, -1, 1);
                 float alpha = getScrollAlpha(progress);
@@ -524,9 +561,9 @@ public class PhotoView extends GLView {
 
         @Override
         public void setScreenNail(ScreenNail s) {
-            mEnabled = (s != null);
             if (mScreenNail == s) return;
             mScreenNail = s;
+            mIsCamera = isCameraScreenNail(s);
             mRotation = mModel.getImageRotation(mIndex);
 
             int w = 0, h = 0;
@@ -549,8 +586,8 @@ public class PhotoView extends GLView {
         }
 
         @Override
-        public boolean isEnabled() {
-            return mEnabled;
+        public boolean isCamera() {
+            return mIsCamera;
         }
     }
 
@@ -677,10 +714,15 @@ public class PhotoView extends GLView {
 
         @Override
         public void onDown() {
+            mHolding |= HOLD_TOUCH_DOWN;
+            if (mPictures.get(0).isCamera()) {
+                mHolding |= HOLD_TOUCH_DOWN_FROM_CAMERA;
+            }
         }
 
         @Override
         public void onUp() {
+            mHolding &= ~(HOLD_TOUCH_DOWN | HOLD_TOUCH_DOWN_FROM_CAMERA);
             mEdgeView.onRelease();
 
             if (mIgnoreUpEvent) {
@@ -688,9 +730,7 @@ public class PhotoView extends GLView {
                 return;
             }
 
-            if (!snapToNeighborImage()) {
-                mPositionController.up();
-            }
+            snapback();
         }
     }
 
@@ -771,7 +811,7 @@ public class PhotoView extends GLView {
 
     // Runs in main thread.
     private void switchFocus() {
-        if (mGestureRecognizer.isDown()) return;
+        if (mHolding != 0) return;
         switch (switchPosition()) {
             case -1:
                 switchToPrevImage();
@@ -788,14 +828,14 @@ public class PhotoView extends GLView {
         Rect curr = mPositionController.getPosition(0);
         int center = getWidth() / 2;
 
-        if (curr.left > center && mPictures.get(-1).isEnabled()) {
+        if (curr.left > center && mPrevBound < 0) {
             Rect prev = mPositionController.getPosition(-1);
             int currDist = curr.left - center;
             int prevDist = center - prev.right;
             if (prevDist < currDist) {
                 return -1;
             }
-        } else if (curr.right < center && mPictures.get(1).isEnabled()) {
+        } else if (curr.right < center && mNextBound > 0) {
             Rect next = mPositionController.getPosition(1);
             int currDist = center - curr.right;
             int nextDist = next.left - center;
@@ -841,6 +881,13 @@ public class PhotoView extends GLView {
         return false;
     }
 
+    private void snapback() {
+        if (mHolding != 0) return;
+        if (!snapToNeighborImage()) {
+            mPositionController.snapback();
+        }
+    }
+
     private boolean snapToNeighborImage() {
         if (mFilmMode) return false;
 
@@ -859,22 +906,16 @@ public class PhotoView extends GLView {
     }
 
     private boolean slideToNextPicture() {
-        Picture next = mPictures.get(1);
-        if (!next.isEnabled()) return false;
-        int currentX = mPositionController.getPosition(1).centerX();
-        int targetX = getWidth() / 2;
-        mPositionController.startHorizontalSlide(targetX - currentX);
+        if (mNextBound <= 0) return false;
         switchToNextImage();
+        mPositionController.startHorizontalSlide();
         return true;
     }
 
     private boolean slideToPrevPicture() {
-        Picture prev = mPictures.get(-1);
-        if (!prev.isEnabled()) return false;
-        int currentX = mPositionController.getPosition(-1).centerX();
-        int targetX = getWidth() / 2;
-        mPositionController.startHorizontalSlide(targetX - currentX);
+        if (mPrevBound >= 0) return false;
         switchToPrevImage();
+        mPositionController.startHorizontalSlide();
         return true;
     }
 
@@ -903,6 +944,44 @@ public class PhotoView extends GLView {
     }
 
     ////////////////////////////////////////////////////////////////////////////
+    //  Capture Animation
+    ////////////////////////////////////////////////////////////////////////////
+
+    public boolean switchWithCaptureAnimation(int offset) {
+        GLRoot root = getGLRoot();
+        root.lockRenderThread();
+        try {
+            return switchWithCaptureAnimationLocked(offset);
+        } finally {
+            root.unlockRenderThread();
+        }
+    }
+
+    private boolean switchWithCaptureAnimationLocked(int offset) {
+        if (mFilmMode) return false;
+        if (mHolding != 0) return true;
+        if (offset == 1) {
+            if (mNextBound <= 0) return false;
+            switchToNextImage();
+            mPositionController.startCaptureAnimationSlide(-1);
+        } else if (offset == -1) {
+            if (mPrevBound >= 0) return false;
+            switchToPrevImage();
+            mPositionController.startCaptureAnimationSlide(1);
+        } else {
+            return false;
+        }
+        mHolding |= HOLD_CAPTURE_ANIMATION;
+        mHandler.sendEmptyMessageDelayed(MSG_CAPTURE_ANIMATION_DONE, 800);
+        return true;
+    }
+
+    private void captureAnimationDone() {
+        mHolding &= ~HOLD_CAPTURE_ANIMATION;
+        snapback();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////
     //  Card deck effect calculation
     ////////////////////////////////////////////////////////////////////////////
 
index 46f0121..e6c132b 100644 (file)
@@ -45,6 +45,7 @@ class PositionController {
     private static final int ANIM_KIND_ZOOM = 4;
     private static final int ANIM_KIND_OPENING = 5;
     private static final int ANIM_KIND_FLING = 6;
+    private static final int ANIM_KIND_CAPTURE = 7;
 
     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
     private static final int ANIM_TIME[] = {
@@ -55,6 +56,7 @@ class PositionController {
         300,  // ANIM_KIND_ZOOM
         600,  // ANIM_KIND_OPENING
         0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
+        800,  // ANIM_KIND_CAPTURE
     };
 
     // We try to scale up the image to fill the screen. But in order not to
@@ -139,7 +141,7 @@ class PositionController {
 
     public interface Listener {
         void invalidate();
-        boolean isDown();
+        boolean isHolding();
 
         // EdgeView
         void onPull(int offset, int direction);
@@ -187,8 +189,8 @@ class PositionController {
     public void setImageSize(int index, int width, int height) {
         if (width == 0 || height == 0) {
             initBox(index);
-        } else {
-            setBoxSize(index, width, height, false);
+        } else if (!setBoxSize(index, width, height, false)) {
+            return;
         }
 
         updateScaleAndGapLimit();
@@ -196,17 +198,18 @@ class PositionController {
         snapAndRedraw();
     }
 
-    private void setBoxSize(int i, int width, int height, boolean isViewSize) {
+    // Returns false if the box size doesn't change.
+    private boolean setBoxSize(int i, int width, int height, boolean isViewSize) {
         Box b = mBoxes.get(i);
         boolean wasViewSize = b.mUseViewSize;
 
         // If we already have an image size, we don't want to use the view size.
-        if (!wasViewSize && isViewSize) return;
+        if (!wasViewSize && isViewSize) return false;
 
         b.mUseViewSize = isViewSize;
 
         if (width == b.mImageW && height == b.mImageH) {
-            return;
+            return false;
         }
 
         // The ratio of the old size and the new size.
@@ -232,6 +235,8 @@ class PositionController {
             mFocusX /= ratio;
             mFocusY /= ratio;
         }
+
+        return true;
     }
 
     private void startOpeningAnimationIfNeeded() {
@@ -339,7 +344,7 @@ class PositionController {
         redraw();
     }
 
-    public void up() {
+    public void snapback() {
         snapAndRedraw();
     }
 
@@ -406,11 +411,26 @@ class PositionController {
         snapAndRedraw();
     }
 
-    public void startHorizontalSlide(int distance) {
+    // Slide the focused box to the center of the view.
+    public void startHorizontalSlide() {
         Box b = mBoxes.get(0);
-        Platform p = mPlatform;
-        startAnimation(getTargetX(p) + distance, getTargetY(b),
-                b.mCurrentScale, ANIM_KIND_SLIDE);
+        startAnimation(mViewW / 2, mViewH / 2, b.mScaleMin, ANIM_KIND_SLIDE);
+    }
+
+    // Slide the focused box to the center of the view with the capture
+    // animation. In addition to the sliding, the animation will also scale the
+    // the focused box, the specified neighbor box, and the gap between the
+    // two. The specified offset should be 1 or -1.
+    public void startCaptureAnimationSlide(int offset) {
+        Box b = mBoxes.get(0);
+        Box n = mBoxes.get(offset);  // the neighbor box
+        Gap g = mGaps.get(offset);  // the gap between the two boxes
+
+        mPlatform.doAnimation(mViewW / 2, ANIM_KIND_CAPTURE);
+        b.doAnimation(mViewH / 2, b.mScaleMin, ANIM_KIND_CAPTURE);
+        n.doAnimation(mViewH / 2, n.mScaleMin, ANIM_KIND_CAPTURE);
+        g.doAnimation(g.mDefaultSize, ANIM_KIND_CAPTURE);
+        redraw();
     }
 
     public void startScroll(float dx, float dy) {
@@ -762,7 +782,7 @@ class PositionController {
     // N N N N N N N -- all new boxes
     // -3 -2 -1 0 1 2 3 -- nothing changed
     // -2 -1 0 1 2 3 N -- focus goes to the next box
-    // N-3 -2 -1 0 1 2 -- focuse goes to the previous box
+    // N -3 -2 -1 0 1 2 -- focuse goes to the previous box
     // -3 -2 -1 1 2 3 N -- the focused box was deleted.
     public void moveBox(int fromIndex[], boolean hasPrev, boolean hasNext) {
         //debugMoveBox(fromIndex);
@@ -837,12 +857,12 @@ class PositionController {
             first = last = 0;
         }
         // Now for those boxes between first and last, just assign the same
-        // position as the previous box. (We can do better, but this should be
+        // position as the next box. (We can do better, but this should be
         // rare). For the boxes before first or after last, we will use a new
         // default gap size below.
-        for (int i = first + 1; i < last; i++) {
+        for (int i = last - 1; i > first; i--) {
             if (from.get(i) != Integer.MAX_VALUE) continue;
-            mBoxes.get(i).mAbsoluteX = mBoxes.get(i - 1).mAbsoluteX;
+            mBoxes.get(i).mAbsoluteX = mBoxes.get(i + 1).mAbsoluteX;
         }
 
         // 7. recycle the gaps that are not used in the new array.
@@ -1089,6 +1109,7 @@ class PositionController {
             switch (kind) {
                 case ANIM_KIND_SCROLL:
                 case ANIM_KIND_FLING:
+                case ANIM_KIND_CAPTURE:
                     progress = 1 - f;  // linear
                     break;
                 case ANIM_KIND_SCALE:
@@ -1116,7 +1137,7 @@ class PositionController {
         public boolean startSnapback() {
             if (mAnimationStartTime != NO_ANIMATION) return false;
             if (mAnimationKind == ANIM_KIND_SCROLL
-                    && mListener.isDown()) return false;
+                    && mListener.isHolding()) return false;
 
             Box b = mBoxes.get(0);
             float scaleMin = mExtraScalingRange ?
@@ -1211,8 +1232,14 @@ class PositionController {
                 mCurrentX = mToX;
                 return true;
             } else {
-                mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
-                return (mCurrentX == mToX);
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    progress = CaptureAnimation.calculateSlide(progress);
+                    mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                    return false;
+                } else {
+                    mCurrentX = (int) (mFromX + progress * (mToX - mFromX));
+                    return (mCurrentX == mToX);
+                }
             }
         }
     }
@@ -1247,7 +1274,7 @@ class PositionController {
         public boolean startSnapback() {
             if (mAnimationStartTime != NO_ANIMATION) return false;
             if (mAnimationKind == ANIM_KIND_SCROLL
-                    && mListener.isDown()) return false;
+                    && mListener.isHolding()) return false;
             if (mInScale && this == mBoxes.get(0)) return false;
 
             int y;
@@ -1288,7 +1315,8 @@ class PositionController {
                 targetY = mViewH / 2;
             }
 
-            if (mCurrentY == targetY && mCurrentScale == targetScale) {
+            if (mCurrentY == targetY && mCurrentScale == targetScale
+                    && kind != ANIM_KIND_CAPTURE) {
                 return false;
             }
 
@@ -1341,7 +1369,13 @@ class PositionController {
             } else {
                 mCurrentY = (int) (mFromY + progress * (mToY - mFromY));
                 mCurrentScale = mFromScale + progress * (mToScale - mFromScale);
-                return (mCurrentY == mToY && mCurrentScale == mToScale);
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentScale *= f;
+                    return false;
+                } else {
+                    return (mCurrentY == mToY && mCurrentScale == mToScale);
+                }
             }
         }
     }
@@ -1361,13 +1395,15 @@ class PositionController {
         @Override
         public boolean startSnapback() {
             if (mAnimationStartTime != NO_ANIMATION) return false;
-            return doAnimation(mDefaultSize);
+            return doAnimation(mDefaultSize, ANIM_KIND_SNAPBACK);
         }
 
         // Starts an animation for a gap.
-        public boolean doAnimation(int targetSize) {
-            if (mCurrentGap == targetSize) return false;
-            mAnimationKind = ANIM_KIND_SNAPBACK;
+        public boolean doAnimation(int targetSize, int kind) {
+            if (mCurrentGap == targetSize && kind != ANIM_KIND_CAPTURE) {
+                return false;
+            }
+            mAnimationKind = kind;
             mFromGap = mCurrentGap;
             mToGap = targetSize;
             mAnimationStartTime = AnimationTime.startTime();
@@ -1383,7 +1419,13 @@ class PositionController {
                 return true;
             } else {
                 mCurrentGap = (int) (mFromGap + progress * (mToGap - mFromGap));
-                return (mCurrentGap == mToGap);
+                if (mAnimationKind == ANIM_KIND_CAPTURE) {
+                    float f = CaptureAnimation.calculateScale(progress);
+                    mCurrentGap = (int) (mCurrentGap * f);
+                    return false;
+                } else {
+                    return (mCurrentGap == mToGap);
+                }
             }
         }
     }