New edge effect when OverScrolled.

The constant force that was applied has been replaced by a spring force
when the OverScroller goes beyond valid position values.

Bounce coefficient can be set for each directions.

Change-Id: If7d506d3f35b3451f590c54d6c04a1deb8d9ca95
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index a30059c..9150b5e2 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -2601,8 +2601,9 @@
         }
 
         void startOverfling(int initialVelocity) {
-            mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, 0, 0, 0, getHeight());
-            edgeReached();
+            final int min = mScrollY > 0 ? Integer.MIN_VALUE : 0;
+            final int max = mScrollY > 0 ? 0 : Integer.MAX_VALUE;
+            mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, min, max, 0, getHeight());
             mTouchMode = TOUCH_MODE_OVERFLING;
             invalidate();
             post(this);
diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java
index 62580245..48e7493 100644
--- a/core/java/android/widget/OverScroller.java
+++ b/core/java/android/widget/OverScroller.java
@@ -42,11 +42,31 @@
     }
 
     /**
-     * Creates a Scroller with the specified interpolator. If the interpolator is
-     * null, the default (viscous) interpolator will be used.
+     * Creates an OverScroller with default edge bounce coefficients.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
      */
     public OverScroller(Context context, Interpolator interpolator) {
+        this(context, interpolator, MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT,
+                MagneticOverScroller.DEFAULT_BOUNCE_COEFFICIENT);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction.
+     */
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY) {
         super(context, interpolator);
+        mOverScrollerX.setBounceCoefficient(bounceCoefficientX);
+        mOverScrollerY.setBounceCoefficient(bounceCoefficientY);
     }
 
     @Override
@@ -69,8 +89,11 @@
      */
     public boolean springback(int startX, int startY, int minX, int maxX, int minY, int maxY) {
         mMode = FLING_MODE;
-        return mOverScrollerX.springback(startX, minX, maxX)
-                || mOverScrollerY.springback(startY, minY, maxY);
+
+        // Make sure both methods are called.
+        final boolean spingbackX = mOverScrollerX.springback(startX, minX, maxX);
+        final boolean spingbackY = mOverScrollerY.springback(startY, minY, maxY);
+        return spingbackX || spingbackY;
     }
 
     @Override
@@ -130,8 +153,8 @@
     }
 
     /**
-     * Returns whether the current Scroller position is overscrolled or still within the minimum and
-     * maximum bounds provided in the
+     * Returns whether the current Scroller is currently returning to a valid position.
+     * Valid bounds were provided by the
      * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
      * 
      * One should check this value before calling
@@ -139,11 +162,13 @@
      * a valid position will then be stopped. The caller has to take into account the fact that the
      * started scroll will start from an overscrolled position.
      * 
-     * @return true when the current position is overscrolled.
+     * @return true when the current position is overscrolled and interpolated back to a valid value.
      */
     public boolean isOverscrolled() {
-        return ((!mOverScrollerX.mFinished && mOverScrollerX.mState != MagneticOverScroller.TO_EDGE) ||
-                (!mOverScrollerY.mFinished && mOverScrollerY.mState != MagneticOverScroller.TO_EDGE));
+        return ((!mOverScrollerX.mFinished &&
+                mOverScrollerX.mState != MagneticOverScroller.TO_EDGE) ||
+                (!mOverScrollerY.mFinished &&
+                        mOverScrollerY.mState != MagneticOverScroller.TO_EDGE));
     }
 
     static class MagneticOverScroller extends Scroller.MagneticScroller {
@@ -156,30 +181,25 @@
         // The allowed overshot distance before boundary is reached.
         private int mOver;
 
-        // When the scroll goes beyond the edges limits, the deceleration is
-        // multiplied by this coefficient, so that the return to a valid
-        // position is faster.
-        private static final float OVERSCROLL_DECELERATION_COEF = 16.0f;
+        // Duration in milliseconds to go back from edge to edge. Springback is half of it.
+        private static final int OVERSCROLL_SPRINGBACK_DURATION = 200;
+
+        // Oscillation period
+        private static final float TIME_COEF =
+            1000.0f * (float) Math.PI / OVERSCROLL_SPRINGBACK_DURATION;
 
         // If the velocity is smaller than this value, no bounce is triggered
         // when the edge limits are reached (would result in a zero pixels
         // displacement anyway).
-        private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 200.0f;
+        private static final float MINIMUM_VELOCITY_FOR_BOUNCE = 140.0f;
 
-        // Could be made public for tuning, but applications would no longer
-        // have the same look and feel.
-        private static final float BOUNCE_COEFFICIENT = 0.4f;
+        // Proportion of the velocity that is preserved when the edge is reached.
+        private static final float DEFAULT_BOUNCE_COEFFICIENT = 0.16f;
 
-        /*
-         * Get a signed deceleration that will reduce the velocity.
-         */
-        @Override
-        float getDeceleration(int velocity) {
-            float decelerationY = super.getDeceleration(velocity);
-            if (mState != TO_EDGE) {
-                decelerationY *= OVERSCROLL_DECELERATION_COEF;
-            }
-            return decelerationY;
+        private float mBounceCoefficient = DEFAULT_BOUNCE_COEFFICIENT;
+
+        void setBounceCoefficient(float coefficient) {
+            mBounceCoefficient = coefficient;
         }
 
         boolean springback(int start, int min, int max) {
@@ -192,20 +212,21 @@
             mDuration = 0;
 
             if (start < min) {
-                startSpringback(start, min, -1);
+                startSpringback(start, min, false);
             } else if (start > max) {
-                startSpringback(start, max, 1);
+                startSpringback(start, max, true);
             }
 
             return !mFinished;
         }
 
-        private void startSpringback(int start, int end, int sign) {
+        private void startSpringback(int start, int end, boolean positive) {
             mFinished = false;
             mState = TO_BOUNCE;
-            mDeceleration = getDeceleration(sign);
-            mFinal = end;
-            mDuration = (int) (1000.0f * Math.sqrt(2.0f * (end - start) / mDeceleration));
+            mStart = mFinal = end;
+            mDuration = OVERSCROLL_SPRINGBACK_DURATION;
+            mStartTime -= OVERSCROLL_SPRINGBACK_DURATION / 2;
+            mVelocity = (int) (Math.abs(end - start) * TIME_COEF * (positive ? 1.0 : -1.0f));
         }
 
         void fling(int start, int velocity, int min, int max, int over) {
@@ -214,39 +235,60 @@
 
             super.fling(start, velocity, min, max);
 
-            if (mStart > max) {
-                if (mStart >= max + over) {
+            if (start > max) {
+                if (start >= max + over) {
                     springback(max + over, min, max);
                 } else {
-                    // Make sure the deceleration brings us back to edge
-                    mVelocity = velocity > 0 ? velocity : -velocity;
-                    mCurrVelocity = velocity;
-                    notifyEdgeReached(start, max, over);
+                    if (velocity <= 0) {
+                        springback(start, min, max);
+                    } else {
+                        long time = AnimationUtils.currentAnimationTimeMillis();
+                        final double durationSinceEdge =
+                            Math.atan((start-max) * TIME_COEF / velocity) / TIME_COEF;
+                        mStartTime = (int) (time - 1000.0f * durationSinceEdge);
+
+                        // Simulate a bounce that started from edge
+                        mStart = max;
+
+                        mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
+
+                        onEdgeReached();
+                    }
                 }
             } else {
-                if (mStart < min) {
-                    if (mStart <= min - over) {
+                if (start < min) {
+                    if (start <= min - over) {
                         springback(min - over, min, max);
                     } else {
-                        // Make sure the deceleration brings us back to edge
-                        mVelocity = velocity < 0 ? velocity : -velocity;
-                        mCurrVelocity = velocity;
-                        notifyEdgeReached(start, min, over);
+                        if (velocity >= 0) {
+                            springback(start, min, max);
+                        } else {
+                            long time = AnimationUtils.currentAnimationTimeMillis();
+                            final double durationSinceEdge =
+                                Math.atan((start-min) * TIME_COEF / velocity) / TIME_COEF;
+                            mStartTime = (int) (time - 1000.0f * durationSinceEdge);
+
+                            // Simulate a bounce that started from edge
+                            mStart = min;
+
+                            mVelocity = (int) (velocity / Math.cos(durationSinceEdge * TIME_COEF));
+
+                            onEdgeReached();
+                        }
+
                     }
                 }
             }
         }
 
         void notifyEdgeReached(int start, int end, int over) {
-            // Compute post-edge deceleration
-            mState = TO_BOUNDARY;
             mDeceleration = getDeceleration(mVelocity);
 
             // Local time, used to compute edge crossing time.
             float timeCurrent = mCurrVelocity / mDeceleration;
             final int distance = end - start;
             float timeEdge = -(float) Math.sqrt((2.0f * distance / mDeceleration)
-                             + (timeCurrent * timeCurrent));
+                    + (timeCurrent * timeCurrent));
 
             mVelocity = (int) (mDeceleration * timeEdge);
 
@@ -261,22 +303,21 @@
             onEdgeReached();
         }
 
-        void onEdgeReached() {
+        private void onEdgeReached() {
             // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
-            mState = TO_BOUNDARY;
-            mDeceleration = getDeceleration(mVelocity);
-
-            int distance = Math.round((mVelocity * mVelocity) / (2.0f * mDeceleration));
+            final float distance = mVelocity / TIME_COEF;
 
             if (Math.abs(distance) < mOver) {
-                // Deceleration will bring us back to final position
+                // Spring force will bring us back to final position
                 mState = TO_BOUNCE;
                 mFinal = mStart;
-                mDuration = (int) (-2000.0f * mVelocity / mDeceleration);
+                mDuration = OVERSCROLL_SPRINGBACK_DURATION;
             } else {
                 // Velocity is too high, we will hit the boundary limit
-                mFinal = mStart + (mVelocity > 0 ? mOver : -mOver);
-                mDuration = computeDuration(mStart, mFinal, mVelocity, mDeceleration);
+                mState = TO_BOUNDARY;
+                int over = mVelocity > 0 ? mOver : -mOver;
+                mFinal = mStart + over;
+                mDuration = (int) (1000.0f * Math.asin(over / distance) / TIME_COEF);
             }
         }
 
@@ -300,26 +341,49 @@
                     break;
                 case TO_BOUNDARY:
                     mStartTime += mDuration;
-                    mStart = mFinal;
-                    mFinal = mStart - (mVelocity > 0 ? mOver : -mOver);
-                    mVelocity = 0;
-                    mDuration = (int) (1000.0f * Math.sqrt(Math.abs(2.0f * mOver / mDeceleration)));
-                    mState = TO_BOUNCE;
+                    startSpringback(mFinal, mFinal - (mVelocity > 0 ? mOver:-mOver), mVelocity > 0);
                     break;
                 case TO_BOUNCE:
-                    float edgeVelocity = mVelocity + mDeceleration * mDuration / 1000.0f;
-                    mVelocity = (int) (-edgeVelocity * BOUNCE_COEFFICIENT);
+                    //mVelocity = (int) (mVelocity * BOUNCE_COEFFICIENT);
+                    mVelocity = (int) (mVelocity * mBounceCoefficient);
                     if (Math.abs(mVelocity) < MINIMUM_VELOCITY_FOR_BOUNCE) {
                         return false;
                     }
-                    mStart = mFinal;
                     mStartTime += mDuration;
-                    mDuration = (int) (-2000.0f * mVelocity / mDeceleration);
                     break;
             }
 
             update();
             return true;
         }
+
+        /*
+         * Update the current position and velocity for current time. Returns
+         * true if update has been done and false if animation duration has been
+         * reached.
+         */
+        @Override
+        boolean update() {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final long duration = time - mStartTime;
+
+            if (duration > mDuration) {
+                return false;
+            }
+
+            double distance;
+            final float t = duration / 1000.0f;
+            if (mState == TO_EDGE) {
+                mCurrVelocity = mVelocity + mDeceleration * t;
+                distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+            } else {
+                final float d = t * TIME_COEF;
+                mCurrVelocity = mVelocity * (float)Math.cos(d);
+                distance = mVelocity / TIME_COEF * Math.sin(d);
+            }
+
+            mCurrentPosition = mStart + (int) distance;
+            return true;
+        }
     }
 }
diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java
index 542866a..6a2f000 100644
--- a/core/java/android/widget/Scroller.java
+++ b/core/java/android/widget/Scroller.java
@@ -431,7 +431,7 @@
         /*
          * Get a signed deceleration that will reduce the velocity.
          */
-        float getDeceleration(int velocity) {
+        static float getDeceleration(int velocity) {
             return velocity > 0 ? -GRAVITY : GRAVITY;
         }