Merge "Added a way to trigger an interpolated rotation of the carousel"
diff --git a/carousel/java/com/android/ex/carousel/CarouselController.java b/carousel/java/com/android/ex/carousel/CarouselController.java
index 7516250..cd29f68 100644
--- a/carousel/java/com/android/ex/carousel/CarouselController.java
+++ b/carousel/java/com/android/ex/carousel/CarouselController.java
@@ -606,6 +606,29 @@
         }
     }
 
+    /**
+     * Triggers a rotation of the carousel. All angles are in card units, see:
+     * {@link CarouselController#setCarouselRotationAngle(float)}) for more details.
+     *
+     * @param endAngle the card unit to which the carousel should rotate to
+     * @param milliseconds the length of the animation
+     * @param interpolationMode three modes are currently supported :
+     * {@link CarouselView.InterpolationMode#LINEAR}
+     * {@link CarouselView.InterpolationMode#DECELERATE_QUADRATIC}
+     * {@link CarouselView.InterpolationMode#ACCELERATE_DECELERATE_CUBIC}
+     * @param maxAnimatedArc the maximum angular distance over which the transition will be
+     * animated.
+     * If the current position is further away, it is set at maxAnimatedArc from endAngle.
+     * This parameter is ignored when <= 0.
+     */
+    public void setCarouselRotationAngle(float endAngle, int milliseconds, int interpolationMode,
+            float maxAnimatedArc) {
+        if (mRenderScript != null) {
+            mRenderScript.setCarouselRotationAngle(endAngle, milliseconds,
+                    interpolationMode, maxAnimatedArc);
+        }
+    }
+
     public void setRadius(float radius) {
         mRadius = radius;
         if (mRenderScript != null) {
diff --git a/carousel/java/com/android/ex/carousel/CarouselRS.java b/carousel/java/com/android/ex/carousel/CarouselRS.java
index 4467fb3..6ab4811 100644
--- a/carousel/java/com/android/ex/carousel/CarouselRS.java
+++ b/carousel/java/com/android/ex/carousel/CarouselRS.java
@@ -631,6 +631,12 @@
         mScript.invoke_setCarouselRotationAngle(theta);
     }
 
+    public void setCarouselRotationAngle(float endAngle, int milliseconds, int interpolationMode,
+            float maxAnimatedArc) {
+        mScript.invoke_setCarouselRotationAngle2(endAngle, milliseconds, interpolationMode,
+                maxAnimatedArc);
+    }
+
     public void setCallback(CarouselCallback callback)
     {
         mCallback = callback;
diff --git a/carousel/java/com/android/ex/carousel/CarouselView.java b/carousel/java/com/android/ex/carousel/CarouselView.java
index 6fd0f20..235387b 100644
--- a/carousel/java/com/android/ex/carousel/CarouselView.java
+++ b/carousel/java/com/android/ex/carousel/CarouselView.java
@@ -62,6 +62,18 @@
     public static final int FILL_DIRECTION_CW = CarouselRS.FILL_DIRECTION_CW;
 
     // Note: remember to update carousel.rs when changing the values below
+    public static class InterpolationMode {
+        /** y= x **/
+        public static final int LINEAR = 0;
+        /** The quadratic curve y= 1 - (1 - x)^2 moves quickly towards the target
+         * while decelerating constantly. **/
+        public static final int DECELERATE_QUADRATIC = 1;
+        /** The cubic curve y= (3-2x)*x^2 gradually accelerates at the origin,
+         * and decelerates near the target. **/
+        public static final int ACCELERATE_DECELERATE_CUBIC = 2;
+    }
+
+    // Note: remember to update carousel.rs when changing the values below
     public static class DetailAlignment {
         /** Detail is centered vertically with respect to the card **/
         public static final int CENTER_VERTICAL = 1;
diff --git a/carousel/java/com/android/ex/carousel/carousel.rs b/carousel/java/com/android/ex/carousel/carousel.rs
index 1646ba1..772e775 100644
--- a/carousel/java/com/android/ex/carousel/carousel.rs
+++ b/carousel/java/com/android/ex/carousel/carousel.rs
@@ -84,6 +84,13 @@
     STATE_LOADED // item was delivered
 };
 
+// Interpolation modes ** THIS LIST MUST MATCH THOSE IN CarouselView.java ***
+enum {
+    INTERPOLATION_LINEAR = 0,
+    INTERPOLATION_DECELERATE_QUADRATIC = 1,
+    INTERPOLATION_ACCELERATE_DECELERATE_CUBIC = 2,
+};
+
 // Detail texture alignments ** THIS LIST MUST MATCH THOSE IN CarouselView.java ***
 enum {
     /** Detail is centered vertically with respect to the card **/
@@ -215,6 +222,7 @@
 static float2 touchPosition; // position of first touch, as defined by last call to doStart(x,y)
 static float velocity = 0.0f;  // angular velocity in radians/s
 static bool overscroll = false; // whether we're in the overscroll animation
+static bool autoscrolling = false; // whether we're in the autoscroll animation
 static bool isDragging = false; // true while the user is dragging the carousel
 static float selectionRadius = 50.0f; // movement greater than this will result in no selection
 static bool enableSelection = false; // enabled until the user drags outside of selectionRadius
@@ -266,6 +274,7 @@
 static float deltaTimeInSeconds(int64_t current);
 static bool rayPlaneIntersect(Ray* ray, Plane* plane, float* tout);
 static bool rayCylinderIntersect(Ray* ray, Cylinder* cylinder, float* tout);
+static void stopAutoscroll();
 
 void init() {
     // initializers currently have a problem when the variables are exported, so initialize
@@ -1090,6 +1099,7 @@
     isDragging = true;
     overscroll = false;
     animatedSelection = doSelection(x, y); // used to provide visual feedback on touch
+    stopAutoscroll();
 }
 
 void doStop(float x, float y, long eventTime)
@@ -1175,6 +1185,100 @@
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
+// Autoscroll Interpolation
+////////////////////////////////////////////////////////////////////////////////////////////////////
+static int64_t autoscrollStartTime = 0L; //tracks when we actually started interpolating
+static int64_t autoscrollDuration = 0L;  //in milli seconds
+static int autoscrollInterpolationMode = INTERPOLATION_LINEAR;
+
+static float autoscrollStopAngle = 0.0f;
+static float autoscrollStartAngle = 0.0f;
+
+void setCarouselRotationAngle2(
+    float endAngle,
+    int   milliseconds,
+    int   interpolationMode,
+    float maxAnimatedArc)
+{
+    float actualStart = radiansToCarouselRotationAngle(bias);
+
+    if (maxAnimatedArc > 0) {
+        //snap the current position to keep end - start under maxAnimatedArc
+        if (actualStart <= endAngle) {
+            if (actualStart < endAngle - maxAnimatedArc) {
+                actualStart = endAngle - maxAnimatedArc;
+            }
+        }
+        else {
+            if (actualStart > endAngle + maxAnimatedArc) {
+                actualStart = endAngle + maxAnimatedArc;
+            }
+        }
+    }
+
+    animating = true;
+    autoscrolling = true;
+    autoscrollDuration = milliseconds;
+    autoscrollInterpolationMode = interpolationMode;
+    autoscrollStartAngle = carouselRotationAngleToRadians(actualStart);
+    autoscrollStopAngle = carouselRotationAngleToRadians(endAngle);
+
+    //Make sure the start and stop angles are in the allowed range
+    const float highBias = maximumBias();
+    const float lowBias  = minimumBias();
+    autoscrollStartAngle = clamp(autoscrollStartAngle, lowBias, highBias);
+    autoscrollStopAngle  = clamp(autoscrollStopAngle, lowBias, highBias);
+
+    //stop other animation kinds
+    overscroll = false;
+    velocity = 0.0f;
+}
+
+static void stopAutoscroll()
+{
+    autoscrolling = false;
+    autoscrollStartTime = 0L; //reset for next time
+}
+
+// This method computes the position of all the cards by updating bias based on a
+// simple interpolation model.  If the cards are still in motion, returns true.
+static bool doAutoscroll(float currentTime)
+{
+    if (autoscrollDuration == 0L) {
+        return false;
+    }
+
+    if (autoscrollStartTime == 0L) {
+        autoscrollStartTime = currentTime;
+    }
+
+    const int64_t interpolationEndTime = autoscrollStartTime + autoscrollDuration;
+
+    float timePos = (currentTime - autoscrollStartTime) / (float)autoscrollDuration;
+    if (timePos > 1.0f) {
+        timePos = 1.0f;
+    }
+
+    float lambda = timePos; //default to linear
+    if (autoscrollInterpolationMode == INTERPOLATION_DECELERATE_QUADRATIC) {
+        lambda = 1.0f - (1.0f - timePos) * (1.0f - timePos);
+    }
+    else if (autoscrollInterpolationMode == INTERPOLATION_ACCELERATE_DECELERATE_CUBIC) {
+        lambda = timePos * timePos * (3 - 2 * timePos);
+    }
+
+    bias = lambda * autoscrollStopAngle + (1.0 - lambda) * autoscrollStartAngle;
+
+    if (currentTime > interpolationEndTime) {
+        stopAutoscroll();
+        return false;
+    }
+    else {
+        return true;
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
 // Hit detection using ray casting.
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 static const float EPSILON = 1.0e-6f;
@@ -1424,7 +1528,7 @@
     return x;
 }
 
-// Computes the next value for bias using the current animation (physics or overscroll)
+// Computes the next value for bias using the current animation (physics/overscroll/autoscrolling)
 static bool updateNextPosition(int64_t currentTime)
 {
     static const float biasMin = 1e-4f; // close enough if we're within this margin of result
@@ -1457,6 +1561,8 @@
         } else {
             overscroll = false;
         }
+    } else if (autoscrolling) {
+        stillAnimating = doAutoscroll(currentTime);
     } else {
         stillAnimating = doPhysics(dt);
         overscroll = bias > highBias || bias < lowBias;