Fix 5319007: Add fling to PhotoView.

Change-Id: Iacda65fbe1fcb3ad245ad99e0b062606ca6792b9
diff --git a/src/com/android/gallery3d/ui/PositionController.java b/src/com/android/gallery3d/ui/PositionController.java
index 8c7fb10..5d66f70 100644
--- a/src/com/android/gallery3d/ui/PositionController.java
+++ b/src/com/android/gallery3d/ui/PositionController.java
@@ -31,18 +31,22 @@
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
+import android.widget.Scroller;
 
 class PositionController {
+    private static final String TAG = "PositionController";
     private long mAnimationStartTime = NO_ANIMATION;
     private static final long NO_ANIMATION = -1;
     private static final long LAST_ANIMATION = -2;
 
     private int mAnimationKind;
+    private float mAnimationDuration;
     private final static int ANIM_KIND_SCROLL = 0;
     private final static int ANIM_KIND_SCALE = 1;
     private final static int ANIM_KIND_SNAPBACK = 2;
     private final static int ANIM_KIND_SLIDE = 3;
     private final static int ANIM_KIND_ZOOM = 4;
+    private final static int ANIM_KIND_FLING = 5;
 
     // Animation time in milliseconds. The order must match ANIM_KIND_* above.
     private final static int ANIM_TIME[] = {
@@ -51,6 +55,7 @@
         600,  // ANIM_KIND_SNAPBACK
         400,  // ANIM_KIND_SLIDE
         300,  // ANIM_KIND_ZOOM
+        0,    // ANIM_KIND_FLING (the duration is calculated dynamically)
     };
 
     // We try to scale up the image to fill the screen. But in order not to
@@ -69,13 +74,20 @@
     private float mCurrentScale, mFromScale, mToScale;
 
     // The focus point of the scaling gesture (in bitmap coordinates).
-    private float mFocusBitmapX;
-    private float mFocusBitmapY;
+    private int mFocusBitmapX;
+    private int mFocusBitmapY;
     private boolean mInScale;
 
     // The minimum and maximum scale we allow.
     private float mScaleMin, mScaleMax = SCALE_LIMIT;
 
+    // This is used by the fling animation
+    private FlingScroller mScroller;
+
+    // The bound of the stable region, see the comments above
+    // calculateStableBound() for details.
+    private int mBoundLeft, mBoundRight, mBoundTop, mBoundBottom;
+
     // Assume the image size is the same as view size before we know the actual
     // size of image.
     private boolean mUseViewSize = true;
@@ -83,8 +95,9 @@
     private RectF mTempRect = new RectF();
     private float[] mTempPoints = new float[8];
 
-    public PositionController(PhotoView viewer) {
+    public PositionController(PhotoView viewer, Context context) {
         mViewer = viewer;
+        mScroller = new FlingScroller();
     }
 
     public void setImageSize(int width, int height) {
@@ -120,6 +133,9 @@
         mToY = translate(mToY, mImageH, height, ratio);
         mToScale = mToScale * ratio;
 
+        mFocusBitmapX = translate(mFocusBitmapX, mImageW, width, ratio);
+        mFocusBitmapY = translate(mFocusBitmapY, mImageH, height, ratio);
+
         mImageW = width;
         mImageH = height;
 
@@ -144,28 +160,14 @@
 
     public void zoomIn(float tapX, float tapY, float targetScale) {
         if (targetScale > mScaleMax) targetScale = mScaleMax;
-        float scale = mCurrentScale;
 
         // Convert the tap position to image coordinate
-        float tempX = (tapX - mViewW / 2) / mCurrentScale + mCurrentX;
-        float tempY = (tapY - mViewH / 2) / mCurrentScale + mCurrentY;
+        int tempX = Math.round((tapX - mViewW / 2) / mCurrentScale + mCurrentX);
+        int tempY = Math.round((tapY - mViewH / 2) / mCurrentScale + mCurrentY);
 
-        // We want to make sure that after zoom-in, we don't see black regions
-        // because we zoom too close to the border. The conditions are:
-        //
-        //  (mViewW / 2) / targetScale + mCurrentX < mImageW
-        // -(mViewW / 2) / targetScale + mCurrentX > 0
-        float min = mViewW / 2.0f / targetScale;
-        float max = mImageW - mViewW / 2.0f / targetScale;
-        int targetX = (int) Utils.clamp(tempX, min, max);
-
-        min = mViewH / 2.0f / targetScale;
-        max = mImageH - mViewH / 2.0f / targetScale;
-        int targetY = (int) Utils.clamp(tempY, min, max);
-
-        // If the width of the image is less then the view, center the image
-        if (mImageW * targetScale < mViewW) targetX = mImageW / 2;
-        if (mImageH * targetScale < mViewH) targetY = mImageH / 2;
+        calculateStableBound(targetScale);
+        int targetX = Utils.clamp(tempX, mBoundLeft, mBoundRight);
+        int targetY = Utils.clamp(tempY, mBoundTop, mBoundBottom);
 
         startAnimation(targetX, targetY, targetScale, ANIM_KIND_ZOOM);
     }
@@ -179,37 +181,31 @@
                 Math.min((float) mViewW / w, (float) mViewH / h));
     }
 
-    // Translate the coordinate if the aspect ratio of the image changes.
-    // When the user slides a image before it's loaded, we don't know the
-    // actual aspect ratio, so we will assume one. When we receive the actual
-    // aspect ratio, we need to translate the coordinate from the old (assumed)
-    // bitmap into the new (actual) bitmap.
+    // Translate a coordinate on bitmap if the bitmap size changes.
+    // If the aspect ratio doesn't change, it's easy:
     //
-    // +-------------------------+  "o" is where center of the view
-    // |          +--------+     |  is. mCurrent{X,Y} is the coordinate of
-    // |          |        |     |  "o" relative to the old bitmap. Assume
-    // |          | o      |     |  the old bitmap size is (w, h).  The new
-    // |          +--------+     |  bitmap size is (w', h'). First we adjust
-    // |                         |  mCurrentScale by factor r = min(w/w',
-    // +-------------------------+  h/h'), so one of the sides matches the old
-    //              |               bitmap (w'*r == w or h'*r == h).
-    //              v
-    // +-------------------------+  Then we put the new scaled bitmap to the
-    // |  +--+    ..........     |  center of the original bitmap's bounding
-    // |  |  |    .        .     |  box. The center of the old bitmap and the
-    // |  |  |    . o      .     |  new bitmap must match in view coordinate:
-    // |  +--+    ..........     |
-    // |                         |  (w/2 - mCurrentX) * mCurrentScale =
-    // +-------------------------+  (w'/2 - mCurrentX') * mCurrentScale * r
-    //              |
-    //              v               Solve for mCurrentX' we have:
-    // +-------------------------+
-    // |          ...+--+...     |  mCurrentX' = w'/2 + (mCurrentX - w/2) / r
-    // |          .  |  |  .     |
-    // |          . o|  |  .     |
-    // |          ...+--+...     |
-    // |                         |
-    // +-------------------------+
+    //         r  = w / w' (= h / h')
+    //         x' = x / r
+    //         y' = y / r
+    //
+    // However the aspect ratio may change. That happens when the user slides
+    // a image before it's loaded, we don't know the actual aspect ratio, so
+    // we will assume one. When we receive the actual bitmap size, we need to
+    // translate the coordinate from the old bitmap into the new bitmap.
+    //
+    // What we want to do is center the bitmap at the original position.
+    //
+    //         ...+--+...
+    //         .  |  |  .
+    //         .  |  |  .
+    //         ...+--+...
+    //
+    // First we scale down the new bitmap by a factor r = min(w/w', h/h').
+    // Overlay it onto the original bitmap. Now (0, 0) of the old bitmap maps
+    // to (-(w-w'*r)/2 / r, -(h-h'*r)/2 / r) in the new bitmap. So (x, y) of
+    // the old bitmap maps to (x', y') in the new bitmap, where
+    //         x' = (x-(w-w'*r)/2) / r = w'/2 + (x-w/2)/r
+    //         y' = (y-(h-h'*r)/2) / r = h'/2 + (y-h/2)/r
     private static int translate(int value, int size, int newSize, float ratio) {
         return Math.round(newSize / 2f + (value - size / 2f) / ratio);
     }
@@ -261,8 +257,10 @@
 
     public void beginScale(float focusX, float focusY) {
         mInScale = true;
-        mFocusBitmapX = mCurrentX + (focusX - mViewW / 2f) / mCurrentScale;
-        mFocusBitmapY = mCurrentY + (focusY - mViewH / 2f) / mCurrentScale;
+        mFocusBitmapX = Math.round(mCurrentX +
+                (focusX - mViewW / 2f) / mCurrentScale);
+        mFocusBitmapY = Math.round(mCurrentY +
+                (focusY - mViewH / 2f) / mCurrentScale);
     }
 
     public void scaleBy(float s, float focusX, float focusY) {
@@ -337,27 +335,51 @@
                 mCurrentScale, type);
     }
 
+    public boolean fling(float velocityX, float velocityY) {
+        // We only want to do fling when the picture is zoomed-in.
+        if (mImageW * mCurrentScale <= mViewW &&
+            mImageH * mCurrentScale <= mViewH) {
+            return false;
+        }
+
+        calculateStableBound(mCurrentScale);
+        mScroller.fling(mCurrentX, mCurrentY,
+                Math.round(-velocityX / mCurrentScale),
+                Math.round(-velocityY / mCurrentScale),
+                mBoundLeft, mBoundRight, mBoundTop, mBoundBottom);
+        int targetX = mScroller.getFinalX();
+        int targetY = mScroller.getFinalY();
+        mAnimationDuration = mScroller.getDuration();
+        startAnimation(targetX, targetY, mCurrentScale, ANIM_KIND_FLING);
+        return true;
+    }
+
     private void startAnimation(
-            int centerX, int centerY, float scale, int kind) {
-        if (centerX == mCurrentX && centerY == mCurrentY
+            int targetX, int targetY, float scale, int kind) {
+        if (targetX == mCurrentX && targetY == mCurrentY
                 && scale == mCurrentScale) return;
 
         mFromX = mCurrentX;
         mFromY = mCurrentY;
         mFromScale = mCurrentScale;
 
-        mToX = centerX;
-        mToY = centerY;
+        mToX = targetX;
+        mToY = targetY;
         mToScale = Utils.clamp(scale, 0.6f * mScaleMin, 1.4f * mScaleMax);
 
-        // If the scaled dimension is smaller than the view,
+        // If the scaled height is smaller than the view height,
         // force it to be in the center.
+        // (We do for height only, not width, because the user may
+        // want to scroll to the previous/next image.)
         if (Math.floor(mImageH * mToScale) <= mViewH) {
             mToY = mImageH / 2;
         }
 
         mAnimationStartTime = SystemClock.uptimeMillis();
         mAnimationKind = kind;
+        if (mAnimationKind != ANIM_KIND_FLING) {
+            mAnimationDuration = ANIM_TIME[mAnimationKind];
+        }
         if (advanceAnimation()) mViewer.invalidate();
     }
 
@@ -375,13 +397,12 @@
             }
         }
 
-        float animationTime = ANIM_TIME[mAnimationKind];
+        long now = SystemClock.uptimeMillis();
         float progress;
-        if (animationTime == 0) {
+        if (mAnimationDuration == 0) {
             progress = 1;
         } else {
-            long now = SystemClock.uptimeMillis();
-            progress = (now - mAnimationStartTime) / animationTime;
+            progress = (now - mAnimationStartTime) / mAnimationDuration;
         }
 
         if (progress >= 1) {
@@ -394,6 +415,7 @@
             float f = 1 - progress;
             switch (mAnimationKind) {
                 case ANIM_KIND_SCROLL:
+                case ANIM_KIND_FLING:
                     progress = 1 - f;  // linear
                     break;
                 case ANIM_KIND_SCALE:
@@ -405,12 +427,23 @@
                     progress = 1 - f * f * f * f * f; // x^5
                     break;
             }
-            linearInterpolate(progress);
+            if (mAnimationKind == ANIM_KIND_FLING) {
+                flingInterpolate(progress);
+            } else {
+                linearInterpolate(progress);
+            }
         }
         mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
         return true;
     }
 
+    private void flingInterpolate(float progress) {
+        mScroller.computeScrollOffset(progress);
+        mCurrentX = mScroller.getCurrX();
+        mCurrentY = mScroller.getCurrY();
+        mViewer.setPosition(mCurrentX, mCurrentY, mCurrentScale);
+    }
+
     // Interpolates mCurrent{X,Y,Scale} given the progress in [0, 1].
     private void linearInterpolate(float progress) {
         // To linearly interpolate the position on view coordinates, we do the
@@ -452,8 +485,6 @@
 
     public boolean startSnapback() {
         boolean needAnimation = false;
-        int x = mCurrentX;
-        int y = mCurrentY;
         float scale = mCurrentScale;
 
         if (mCurrentScale < mScaleMin || mCurrentScale > mScaleMax) {
@@ -461,37 +492,12 @@
             scale = Utils.clamp(mCurrentScale, mScaleMin, mScaleMax);
         }
 
-        // The number of pixels between the center of the view
-        // and the edge when the edge is aligned.
-        int left = (int) Math.ceil(mViewW / (2 * scale));
-        int right = mImageW - left;
-        int top = (int) Math.ceil(mViewH / (2 * scale));
-        int bottom = mImageH - top;
+        calculateStableBound(scale);
+        int x = Utils.clamp(mCurrentX, mBoundLeft, mBoundRight);
+        int y = Utils.clamp(mCurrentY, mBoundTop, mBoundBottom);
 
-        if (mImageW * scale > mViewW) {
-            if (mCurrentX < left) {
-                needAnimation = true;
-                x = left;
-            } else if (mCurrentX > right) {
-                needAnimation = true;
-                x = right;
-            }
-        } else if (mCurrentX != mImageW / 2) {
+        if (mCurrentX != x || mCurrentY != y || mCurrentScale != scale) {
             needAnimation = true;
-            x = mImageW / 2;
-        }
-
-        if (mImageH * scale > mViewH) {
-            if (mCurrentY < top) {
-                needAnimation = true;
-                y = top;
-            } else if (mCurrentY > bottom) {
-                needAnimation = true;
-                y = bottom;
-            }
-        } else if (mCurrentY != mImageH / 2) {
-            needAnimation = true;
-            y = mImageH / 2;
         }
 
         if (needAnimation) {
@@ -501,22 +507,54 @@
         return needAnimation;
     }
 
+    // Calculates the stable region of mCurrent{X/Y}, where "stable" means
+    //
+    // (1) If the dimension of scaled image >= view dimension, we will not
+    // see black region outside the image (at that dimension).
+    // (2) If the dimension of scaled image < view dimension, we will center
+    // the scaled image.
+    //
+    // We might temporarily go out of this stable during user interaction,
+    // but will "snap back" after user stops interaction.
+    //
+    // The results are stored in mBound{Left/Right/Top/Bottom}.
+    //
+    private void calculateStableBound(float scale) {
+        // The number of pixels between the center of the view
+        // and the edge when the edge is aligned.
+        mBoundLeft = (int) Math.ceil(mViewW / (2 * scale));
+        mBoundRight = mImageW - mBoundLeft;
+        mBoundTop = (int) Math.ceil(mViewH / (2 * scale));
+        mBoundBottom = mImageH - mBoundTop;
+
+        // If the scaled height is smaller than the view height,
+        // force it to be in the center.
+        if (Math.floor(mImageH * scale) <= mViewH) {
+            mBoundTop = mBoundBottom = mImageH / 2;
+        }
+
+        // Same for width
+        if (Math.floor(mImageW * scale) <= mViewW) {
+            mBoundLeft = mBoundRight = mImageW / 2;
+        }
+    }
+
+    private boolean useCurrentValueAsTarget() {
+        return mAnimationStartTime == NO_ANIMATION ||
+                mAnimationKind == ANIM_KIND_SNAPBACK ||
+                mAnimationKind == ANIM_KIND_FLING;
+    }
+
     private float getTargetScale() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentScale;
-        return mToScale;
+        return useCurrentValueAsTarget() ? mCurrentScale : mToScale;
     }
 
     private int getTargetX() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentX;
-        return mToX;
+        return useCurrentValueAsTarget() ? mCurrentX : mToX;
     }
 
     private int getTargetY() {
-        if (mAnimationStartTime == NO_ANIMATION
-                || mAnimationKind == ANIM_KIND_SNAPBACK) return mCurrentY;
-        return mToY;
+        return useCurrentValueAsTarget() ? mCurrentY : mToY;
     }
 
     public RectF getImageBounds() {