Fix 5223982: Add animation when scrolling hits the edge.

Change-Id: I3c5191af3fe44ba835ae9b22755613a933065bcd
diff --git a/src/com/android/gallery3d/ui/Paper.java b/src/com/android/gallery3d/ui/Paper.java
index 641fc2c..ecc4150 100644
--- a/src/com/android/gallery3d/ui/Paper.java
+++ b/src/com/android/gallery3d/ui/Paper.java
@@ -16,10 +16,14 @@
 
 package com.android.gallery3d.ui;
 
+import com.android.gallery3d.common.Utils;
 import com.android.gallery3d.ui.PositionRepository.Position;
 import com.android.gallery3d.util.GalleryUtils;
 
 import android.opengl.Matrix;
+import android.os.SystemClock;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
 
 import javax.microedition.khronos.opengles.GL11;
 import javax.microedition.khronos.opengles.GL11ExtensionPack;
@@ -28,22 +32,37 @@
 class Paper {
     private static final String TAG = "Paper";
     private static final int ROTATE_FACTOR = 4;
-    private OverscrollAnimation mAnimationLeft = new OverscrollAnimation();
-    private OverscrollAnimation mAnimationRight = new OverscrollAnimation();
+    private EdgeAnimation mAnimationLeft = new EdgeAnimation();
+    private EdgeAnimation mAnimationRight = new EdgeAnimation();
     private int mWidth, mHeight;
     private float[] mMatrix = new float[16];
 
     public void overScroll(float distance) {
+        distance /= mWidth;  // make it relative to width
         if (distance < 0) {
-            mAnimationLeft.scroll(-distance);
+            mAnimationLeft.onPull(-distance);
         } else {
-            mAnimationRight.scroll(distance);
+            mAnimationRight.onPull(distance);
         }
     }
 
-    public boolean advanceAnimation(long currentTimeMillis) {
-        return mAnimationLeft.advanceAnimation(currentTimeMillis)
-            | mAnimationRight.advanceAnimation(currentTimeMillis);
+    public void edgeReached(float velocity) {
+        velocity /= mWidth;  // make it relative to width
+        if (velocity < 0) {
+            mAnimationRight.onAbsorb(-velocity);
+        } else {
+            mAnimationLeft.onAbsorb(velocity);
+        }
+    }
+
+    public void onRelease() {
+        mAnimationLeft.onRelease();
+        mAnimationRight.onRelease();
+    }
+
+    public boolean advanceAnimation() {
+        // Note that we use "|" because we want both animations get updated.
+        return mAnimationLeft.update() | mAnimationRight.update();
     }
 
     public void setSize(int width, int height) {
@@ -56,7 +75,12 @@
         float left = mAnimationLeft.getValue();
         float right = mAnimationRight.getValue();
         float screenX = target.x - scrollX;
-        float t = ((mWidth - screenX) * left - screenX * right) / (mWidth * mWidth);
+        // We linearly interpolate the value [left, right] for the screenX
+        // range int [-1/4, 5/4]*mWidth. So if part of the thumbnail is outside
+        // the screen, we still get some transform.
+        float x = screenX + mWidth / 4;
+        int range = 3 * mWidth / 2;
+        float t = ((range - x) * left - x * right) / range;
         // compress t to the range (-1, 1) by the function
         // f(t) = (1 / (1 + e^-t) - 0.5) * 2
         // then multiply by 90 to make the range (-45, 45)
@@ -71,42 +95,96 @@
     }
 }
 
-class OverscrollAnimation {
-    private static final String TAG = "OverscrollAnimation";
-    private static final long START_ANIMATION = -1;
-    private static final long NO_ANIMATION = -2;
-    private static final long ANIMATION_DURATION = 500;
+// This class follows the structure of frameworks's EdgeEffect class.
+class EdgeAnimation {
+    private static final String TAG = "EdgeAnimation";
 
-    private long mAnimationStartTime = NO_ANIMATION;
-    private float mVelocity;
-    private float mCurrentValue;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RELEASE = 3;
 
-    public void scroll(float distance) {
-        mAnimationStartTime = START_ANIMATION;
-        mCurrentValue += distance;
+    // Time it will take the effect to fully done in ms
+    private static final int ABSORB_TIME = 200;
+    private static final int RELEASE_TIME = 500;
+
+    private static final float VELOCITY_FACTOR = 0.1f;
+
+    private final Interpolator mInterpolator;
+
+    private int mState;
+    private long mAnimationStartTime;
+    private float mValue;
+
+    private float mValueStart;
+    private float mValueFinish;
+    private long mStartTime;
+    private long mDuration;
+
+    public EdgeAnimation() {
+        mInterpolator = new DecelerateInterpolator();
+        mState = STATE_IDLE;
     }
 
-    public boolean advanceAnimation(long currentTimeMillis) {
-        if (mAnimationStartTime == NO_ANIMATION) return false;
-        if (mAnimationStartTime == START_ANIMATION) {
-            mAnimationStartTime = currentTimeMillis;
-            return true;
+    private void startAnimation(float start, float finish, long duration,
+            int newState) {
+        mValueStart = start;
+        mValueFinish = finish;
+        mDuration = duration;
+        mStartTime = now();
+        mState = newState;
+    }
+
+    // The deltaDistance's magnitude is in the range of -1 (no change) to 1.
+    // The value 1 is the full length of the view. Negative values means the
+    // movement is in the opposite direction.
+    public void onPull(float deltaDistance) {
+        if (mState == STATE_ABSORB) return;
+        mValue = Utils.clamp(mValue + deltaDistance, -1.0f, 1.0f);
+        mState = STATE_PULL;
+    }
+
+    public void onRelease() {
+        if (mState == STATE_IDLE || mState == STATE_ABSORB) return;
+        startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+    }
+
+    public void onAbsorb(float velocity) {
+        float finish = Utils.clamp(mValue + velocity * VELOCITY_FACTOR,
+                -1.0f, 1.0f);
+        startAnimation(mValue, finish, ABSORB_TIME, STATE_ABSORB);
+    }
+
+    public boolean update() {
+        if (mState == STATE_IDLE) return false;
+        if (mState == STATE_PULL) return true;
+
+        float t = Utils.clamp((float)(now() - mStartTime) / mDuration, 0.0f, 1.0f);
+        /* Use linear interpolation for absorb, quadratic for others */
+        float interp = (mState == STATE_ABSORB)
+                ? t : mInterpolator.getInterpolation(t);
+
+        mValue = mValueStart + (mValueFinish - mValueStart) * interp;
+
+        if (t >= 1.0f) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    startAnimation(mValue, 0, RELEASE_TIME, STATE_RELEASE);
+                    break;
+                case STATE_RELEASE:
+                    mState = STATE_IDLE;
+                    break;
+            }
         }
 
-        long deltaTime = currentTimeMillis - mAnimationStartTime;
-        float t = deltaTime / 100f;
-        mCurrentValue *= Math.pow(0.5f, t);
-        mAnimationStartTime = currentTimeMillis;
-
-        if (mCurrentValue < 1) {
-            mAnimationStartTime = NO_ANIMATION;
-            mCurrentValue = 0;
-            return false;
-        }
         return true;
     }
 
     public float getValue() {
-        return mCurrentValue;
+        return mValue;
+    }
+
+    private long now() {
+        return SystemClock.uptimeMillis();
     }
 }
diff --git a/src/com/android/gallery3d/ui/ScrollerHelper.java b/src/com/android/gallery3d/ui/ScrollerHelper.java
index 9f19cec..8423518 100644
--- a/src/com/android/gallery3d/ui/ScrollerHelper.java
+++ b/src/com/android/gallery3d/ui/ScrollerHelper.java
@@ -58,6 +58,10 @@
         return mScroller.getCurrX();
     }
 
+    public float getCurrVelocity() {
+        return mScroller.getCurrVelocity();
+    }
+
     public void setPosition(int position) {
         mScroller.startScroll(
                 position, 0,    // startX, startY
@@ -77,7 +81,8 @@
                 mOverflingEnabled ? mOverflingDistance : 0, 0);
     }
 
-    public boolean startScroll(int distance, int min, int max) {
+    // Returns the distance that over the scroll limit.
+    public int startScroll(int distance, int min, int max) {
         int currPosition = mScroller.getCurrX();
         int finalPosition = mScroller.getFinalX();
         int newPosition = Utils.clamp(finalPosition + distance, min, max);
@@ -85,9 +90,7 @@
             mScroller.startScroll(
                 currPosition, 0,                    // startX, startY
                 newPosition - currPosition, 0, 0);  // dx, dy, duration
-            return true;
-        } else {
-            return false;
         }
+        return finalPosition + distance - newPosition;
     }
 }
diff --git a/src/com/android/gallery3d/ui/SlotView.java b/src/com/android/gallery3d/ui/SlotView.java
index 3a4de39..4b0dc29 100644
--- a/src/com/android/gallery3d/ui/SlotView.java
+++ b/src/com/android/gallery3d/ui/SlotView.java
@@ -147,7 +147,15 @@
     @Override
     protected void onLayout(boolean changeSize, int l, int t, int r, int b) {
         if (!changeSize) return;
+
+        // Make sure we are still at a resonable scroll position after the size
+        // is changed (like orientation change). We choose to keep the center
+        // visible slot still visible. This is arbitrary but reasonable.
+        int visibleIndex =
+                (mLayout.getVisibleStart() + mLayout.getVisibleEnd()) / 2;
         mLayout.setSize(r - l, b - t);
+        makeSlotVisible(visibleIndex);
+
         onLayoutChanged(r - l, b - t);
         if (mOverscrollEffect == OVERSCROLL_3D) {
             mPaper.setSize(r - l, b - t);
@@ -219,6 +227,10 @@
                 mDownInScrolling = !mScroller.isFinished();
                 mScroller.forceFinished();
                 break;
+            case MotionEvent.ACTION_UP:
+                mPaper.onRelease();
+                invalidate();
+                break;
         }
         return true;
     }
@@ -242,17 +254,30 @@
 
         long currentTimeMillis = canvas.currentAnimationTimeMillis();
         boolean more = mScroller.advanceAnimation(currentTimeMillis);
-        boolean paperActive = (mOverscrollEffect == OVERSCROLL_3D)
-                && mPaper.advanceAnimation(currentTimeMillis);
+        int oldX = mScrollX;
         updateScrollPosition(mScroller.getPosition(), false);
+
+        boolean paperActive = false;
+        if (mOverscrollEffect == OVERSCROLL_3D) {
+            // Check if an edge is reached and notify mPaper if so.
+            int newX = mScrollX;
+            int limit = mLayout.getScrollLimit();
+            if (oldX > 0 && newX == 0 || oldX < limit && newX == limit) {
+                float v = mScroller.getCurrVelocity();
+                if (newX == limit) v = -v;
+                mPaper.edgeReached(v);
+            }
+            paperActive = mPaper.advanceAnimation();
+        }
+
+        more |= paperActive;
+
         float interpolate = 1f;
         if (mAnimation != null) {
             more |= mAnimation.calculate(currentTimeMillis);
             interpolate = mAnimation.value;
         }
 
-        more |= paperActive;
-
         if (WIDE) {
             canvas.translate(-mScrollX, 0, 0);
         } else {
@@ -643,10 +668,10 @@
                 MotionEvent e2, float distanceX, float distanceY) {
             cancelDown();
             float distance = WIDE ? distanceX : distanceY;
-            boolean canMove = mScroller.startScroll(
+            int overDistance = mScroller.startScroll(
                     Math.round(distance), 0, mLayout.getScrollLimit());
-            if (mOverscrollEffect == OVERSCROLL_3D && !canMove) {
-                mPaper.overScroll(distance);
+            if (mOverscrollEffect == OVERSCROLL_3D && overDistance != 0) {
+                mPaper.overScroll(overDistance);
             }
             invalidate();
             return true;