Refactor and generalize SpringAnimationHandler.
am: 5c83e7cdc5

Change-Id: I3fa69b3ab59badf589657a1dfc539481710e34b8
diff --git a/res/values/config.xml b/res/values/config.xml
index d2272f2..db1a75d 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -64,6 +64,9 @@
     <!-- The duration of the animation from search hint to text entry -->
     <integer name="config_searchHintAnimationDuration">50</integer>
 
+    <!-- View tag key used to store SpringAnimation data. -->
+    <item type="id" name="spring_animation_tag" />
+
 <!-- Workspace -->
     <!-- The duration (in ms) of the fade animation on the object outlines, used when
          we are dragging objects around on the home screen. -->
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index f1616fc..c3df073 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -91,9 +91,8 @@
 
         mLauncher = Launcher.getLauncher(context);
         mApps = new AlphabeticalAppsList(context);
-        mSpringAnimationHandler = new SpringAnimationHandler(SpringAnimationHandler.Y_DIRECTION);
-        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this,
-                mSpringAnimationHandler);
+        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
+        mSpringAnimationHandler = mAdapter.getSpringAnimationHandler();
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mSearchQueryBuilder = new SpannableStringBuilder();
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index d3d23ca..9c7372f 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -19,6 +19,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.Point;
+import android.support.animation.DynamicAnimation;
 import android.support.animation.SpringAnimation;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
@@ -38,6 +39,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
@@ -96,11 +98,6 @@
      */
     public static class ViewHolder extends RecyclerView.ViewHolder {
 
-        /**
-         * Springs used for items where isViewType(viewType, VIEW_TYPE_MASK_HAS_SPRINGS) is true.
-         */
-        private SpringAnimation spring;
-
         public ViewHolder(View v) {
             super(v);
         }
@@ -213,11 +210,10 @@
     // The intent to send off to the market app, updated each time the search query changes.
     private Intent mMarketSearchIntent;
 
-    private SpringAnimationHandler mSpringAnimationHandler;
+    private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
 
     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
-            iconClickListener, View.OnLongClickListener iconLongClickListener,
-            SpringAnimationHandler springAnimationHandler) {
+            iconClickListener, View.OnLongClickListener iconLongClickListener) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -228,7 +224,14 @@
         mLayoutInflater = LayoutInflater.from(launcher);
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
-        mSpringAnimationHandler = springAnimationHandler;
+        if (FeatureFlags.LAUNCHER3_PHYSICS) {
+            mSpringAnimationHandler = new SpringAnimationHandler<>(
+                    SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
+        }
+    }
+
+    public SpringAnimationHandler getSpringAnimationHandler() {
+        return mSpringAnimationHandler;
     }
 
     public static boolean isDividerViewType(int viewType) {
@@ -292,8 +295,7 @@
                         R.layout.all_apps_icon, parent, false);
                 icon.setOnClickListener(mIconClickListener);
                 icon.setOnLongClickListener(mIconLongClickListener);
-                icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
-                        .getLongPressTimeout());
+                icon.setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
                 icon.setOnFocusChangeListener(mIconFocusListener);
 
                 // Ensure the all apps icon height matches the workspace icons
@@ -386,8 +388,7 @@
     public void onViewAttachedToWindow(ViewHolder holder) {
         int type = holder.getItemViewType();
         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
-            holder.spring = mSpringAnimationHandler.add(holder.itemView,
-                    holder.getAdapterPosition(), mApps, mAppsPerRow, holder.spring);
+            mSpringAnimationHandler.add(holder.itemView, holder);
         }
     }
 
@@ -395,7 +396,7 @@
     public void onViewDetachedFromWindow(ViewHolder holder) {
         int type = holder.getItemViewType();
         if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
-            holder.spring = mSpringAnimationHandler.remove(holder.spring);
+            mSpringAnimationHandler.remove(holder.itemView);
         }
     }
 
@@ -415,4 +416,121 @@
         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
         return item.viewType;
     }
+
+    /**
+     * Helper class to set the SpringAnimation values for an item in the adapter.
+     */
+    private class AllAppsSpringAnimationFactory
+            implements SpringAnimationHandler.AnimationFactory<ViewHolder> {
+        private static final float DEFAULT_MAX_VALUE_PX = 100;
+        private static final float DEFAULT_MIN_VALUE_PX = -DEFAULT_MAX_VALUE_PX;
+
+        // Damping ratio range is [0, 1]
+        private static final float SPRING_DAMPING_RATIO = 0.55f;
+
+        // Stiffness is a non-negative number.
+        private static final float MIN_SPRING_STIFFNESS = 580f;
+        private static final float MAX_SPRING_STIFFNESS = 900f;
+
+        // The amount by which each adjacent rows' stiffness will differ.
+        private static final float ROW_STIFFNESS_COEFFICIENT = 50f;
+
+        @Override
+        public SpringAnimation initialize(ViewHolder vh) {
+            return SpringAnimationHandler.forView(vh.itemView, DynamicAnimation.TRANSLATION_Y, 0);
+        }
+
+        /**
+         * @param spring A new or recycled SpringAnimation.
+         * @param vh The ViewHolder that {@param spring} is related to.
+         */
+        @Override
+        public void update(SpringAnimation spring, ViewHolder vh) {
+            int numPredictedApps = Math.min(mAppsPerRow, mApps.getPredictedApps().size());
+            int appPosition = getAppPosition(vh.getAdapterPosition(), numPredictedApps,
+                    mAppsPerRow);
+
+            int col = appPosition % mAppsPerRow;
+            int row = appPosition / mAppsPerRow;
+
+            int numTotalRows = mApps.getNumAppRows() - 1; // zero-based count
+            if (row > (numTotalRows / 2)) {
+                // Mirror the rows so that the top row acts the same as the bottom row.
+                row = Math.abs(numTotalRows - row);
+            }
+
+            // We manipulate the stiffness, min, and max values based on the items distance to the
+            // first row and the items distance to the center column to create the ^-shaped motion
+            // effect.
+            float rowFactor = (1 + row) * 0.5f;
+            float colFactor = getColumnFactor(col, mAppsPerRow);
+
+            float minValue = DEFAULT_MIN_VALUE_PX * (rowFactor + colFactor);
+            float maxValue = DEFAULT_MAX_VALUE_PX * (rowFactor + colFactor);
+
+            float stiffness = Utilities.boundToRange(
+                    MAX_SPRING_STIFFNESS - (row * ROW_STIFFNESS_COEFFICIENT),
+                    MIN_SPRING_STIFFNESS,
+                    MAX_SPRING_STIFFNESS);
+
+            spring.setMinValue(minValue)
+                    .setMaxValue(maxValue)
+                    .getSpring()
+                    .setStiffness(stiffness)
+                    .setDampingRatio(SPRING_DAMPING_RATIO);
+        }
+
+        /**
+         * @return The app position is the position of the app in the Adapter if we ignored all
+         * other view types.
+         *
+         * The first app is at position 0, and the first app each following row is at a
+         * position that is a multiple of {@param appsPerRow}.
+         *
+         * ie. If there are 5 apps per row, and there are two rows of apps:
+         *     0 1 2 3 4
+         *     5 6 7 8 9
+         */
+        private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
+            int appPosition = position;
+            int numDividerViews = 1 + (numPredictedApps == 0 ? 0 : 1);
+
+            int allAppsStartAt = numDividerViews + numPredictedApps;
+            if (numDividerViews == 1 || position < allAppsStartAt) {
+                appPosition -= 1;
+            } else {
+                // We cannot assume that the predicted row will always be full.
+                int numPredictedAppsOffset = appsPerRow - numPredictedApps;
+                appPosition = position + numPredictedAppsOffset - numDividerViews;
+            }
+
+            return appPosition;
+        }
+
+        /**
+         * Increase the column factor as the distance increases between the column and the center
+         * column(s).
+         */
+        private float getColumnFactor(int col, int numCols) {
+            float centerColumn = numCols / 2;
+            int distanceToCenter = (int) Math.abs(col - centerColumn);
+
+            boolean evenNumberOfColumns = numCols % 2 == 0;
+            if (evenNumberOfColumns && col < centerColumn) {
+                distanceToCenter -= 1;
+            }
+
+            float factor = 0;
+            while (distanceToCenter > 0) {
+                if (distanceToCenter == 1) {
+                    factor += 0.2f;
+                } else {
+                    factor += 0.1f;
+                }
+                --distanceToCenter;
+            }
+
+            return factor;
+        }
+    }
 }
diff --git a/src/com/android/launcher3/anim/SpringAnimationHandler.java b/src/com/android/launcher3/anim/SpringAnimationHandler.java
index 6a5e351..038f826 100644
--- a/src/com/android/launcher3/anim/SpringAnimationHandler.java
+++ b/src/com/android/launcher3/anim/SpringAnimationHandler.java
@@ -15,7 +15,7 @@
  */
 package com.android.launcher3.anim;
 
-import android.support.animation.DynamicAnimation;
+import android.support.animation.FloatPropertyCompat;
 import android.support.animation.SpringAnimation;
 import android.support.animation.SpringForce;
 import android.support.annotation.IntDef;
@@ -24,8 +24,7 @@
 import android.view.VelocityTracker;
 import android.view.View;
 
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.AlphabeticalAppsList;
+import com.android.launcher3.R;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -35,77 +34,67 @@
  * Handler class that manages springs for a set of views that should all move based on the same
  * {@link MotionEvent}s.
  *
- * Supports using physics for X or Y translations.
+ * Supports setting either X or Y velocity on the list of springs added to this handler.
  */
-public class SpringAnimationHandler {
+public class SpringAnimationHandler<T> {
 
     private static final String TAG = "SpringAnimationHandler";
     private static final boolean DEBUG = false;
 
-    private static final float DEFAULT_MAX_VALUE = 100;
-    private static final float DEFAULT_MIN_VALUE = -DEFAULT_MAX_VALUE;
-
-    private static final float SPRING_DAMPING_RATIO = 0.55f;
-    private static final float MIN_SPRING_STIFFNESS = 580f;
-    private static final float MAX_SPRING_STIFFNESS = 900f;
+    private static final float VELOCITY_DAMPING_FACTOR = 0.175f;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef({Y_DIRECTION, X_DIRECTION})
     public @interface Direction {}
     public static final int Y_DIRECTION = 0;
     public static final int X_DIRECTION = 1;
-    private int mDirection;
+    private int mVelocityDirection;
 
     private VelocityTracker mVelocityTracker;
     private float mCurrentVelocity = 0;
     private boolean mShouldComputeVelocity = false;
 
+    private AnimationFactory<T> mAnimationFactory;
+
     private ArrayList<SpringAnimation> mAnimations = new ArrayList<>();
 
-    public SpringAnimationHandler(@Direction int direction) {
-        mDirection = direction;
-        mVelocityTracker = VelocityTracker.obtain();
+    /**
+     * @param direction Either {@link #X_DIRECTION} or {@link #Y_DIRECTION}.
+     *                  Determines which direction we use to calculate and set the velocity.
+     * @param factory   The AnimationFactory is responsible for initializing and updating the
+     *                  SpringAnimations added to this class.
+     */
+    public SpringAnimationHandler(@Direction int direction, AnimationFactory<T> factory) {
+        mVelocityDirection = direction;
+        mAnimationFactory = factory;
     }
 
-    public SpringAnimation add(View view, int position, AlphabeticalAppsList apps, int appsPerRow,
-            SpringAnimation recycle) {
-        int numPredictedApps = Math.min(appsPerRow, apps.getPredictedApps().size());
-        int appPosition = getAppPosition(position, numPredictedApps, appsPerRow);
-
-        int col = appPosition % appsPerRow;
-        int row = appPosition / appsPerRow;
-
-        int numTotalRows = apps.getNumAppRows() - 1; // zero offset
-        if (row > (numTotalRows / 2)) {
-            // Mirror the rows so that the top row acts the same as the bottom row.
-            row = Math.abs(numTotalRows - row);
+    /**
+     * Adds a new or recycled animation to the list of springs handled by this class.
+     *
+     * @param view The view the spring is attached to.
+     * @param object Used to initialize and update the spring.
+     */
+    public void add(View view, T object) {
+        SpringAnimation spring = (SpringAnimation) view.getTag(R.id.spring_animation_tag);
+        if (spring == null) {
+            spring = mAnimationFactory.initialize(object);
+            view.setTag(R.id.spring_animation_tag, spring);
         }
-
-        // We manipulate the stiffness, min, and max values based on the items distance to the first
-        // row and the items distance to the center column to create the ^-shaped motion effect.
-        float rowFactor = (1 + row) * 0.5f;
-        float colFactor = getColumnFactor(col, appsPerRow);
-
-        float minValue = DEFAULT_MIN_VALUE * (rowFactor + colFactor);
-        float maxValue = DEFAULT_MAX_VALUE * (rowFactor + colFactor);
-
-        float stiffness = Utilities.boundToRange(MAX_SPRING_STIFFNESS - (row * 50f),
-                MIN_SPRING_STIFFNESS, MAX_SPRING_STIFFNESS);
-
-        SpringAnimation animation = (recycle != null ? recycle : createSpringAnimation(view))
-                .setStartVelocity(mCurrentVelocity)
-                .setMinValue(minValue)
-                .setMaxValue(maxValue);
-        animation.getSpring().setStiffness(stiffness);
-
-        mAnimations.add(animation);
-        return animation;
+        mAnimationFactory.update(spring, object);
+        spring.setStartVelocity(mCurrentVelocity);
+        mAnimations.add(spring);
     }
 
-    public SpringAnimation remove(SpringAnimation animation) {
-        animation.skipToEnd();
+    /**
+     * Stops and removes the spring attached to {@param view}.
+     */
+    public void remove(View view) {
+        SpringAnimation animation = (SpringAnimation) view.getTag(R.id.spring_animation_tag);
+        if (animation.canSkipToEnd()) {
+            animation.skipToEnd();
+        }
         mAnimations.remove(animation);
-        return animation;
     }
 
     public void addMovement(MotionEvent event) {
@@ -149,7 +138,9 @@
 
         int size = mAnimations.size();
         for (int i = 0; i < size; ++i) {
-            mAnimations.get(i).skipToEnd();
+            if (mAnimations.get(i).canSkipToEnd()) {
+                mAnimations.get(i).skipToEnd();
+            }
         }
     }
 
@@ -169,78 +160,19 @@
     }
 
     private void computeVelocity() {
-        getVelocityTracker().computeCurrentVelocity(175);
+        getVelocityTracker().computeCurrentVelocity(1000 /* millis */);
 
         mCurrentVelocity = isVerticalDirection()
                 ? getVelocityTracker().getYVelocity()
                 : getVelocityTracker().getXVelocity();
+        mCurrentVelocity *= VELOCITY_DAMPING_FACTOR;
         mShouldComputeVelocity = false;
 
         if (DEBUG) Log.d(TAG, "computeVelocity=" + mCurrentVelocity);
     }
 
     private boolean isVerticalDirection() {
-        return mDirection == Y_DIRECTION;
-    }
-
-    private SpringAnimation createSpringAnimation(View view) {
-        DynamicAnimation.ViewProperty property = isVerticalDirection()
-                ? DynamicAnimation.TRANSLATION_Y
-                : DynamicAnimation.TRANSLATION_X;
-
-        return new SpringAnimation(view, property, 0)
-                .setStartValue(1f)
-                .setSpring(new SpringForce(0)
-                .setDampingRatio(SPRING_DAMPING_RATIO));
-    }
-
-    /**
-     * @return The app position is the position of the app in the Adapter if we ignored all other
-     * view types.
-     *
-     * ie. The first predicted app is at position 0, and the first app of all apps is
-     *     at {@param appsPerRow}.
-     */
-    private int getAppPosition(int position, int numPredictedApps, int appsPerRow) {
-        int appPosition = position;
-        int numDividerViews = 1 + (numPredictedApps == 0 ? 0 : 1);
-
-        int allAppsStartAt = numDividerViews + numPredictedApps;
-        if (numDividerViews == 1 || position < allAppsStartAt) {
-            appPosition -= 1;
-        } else {
-            // We cannot assume that the predicted row will always be full.
-            int numPredictedAppsOffset = appsPerRow - numPredictedApps;
-            appPosition = position + numPredictedAppsOffset - numDividerViews;
-        }
-
-        return appPosition;
-    }
-
-    /**
-     * Increase the column factor as the distance increases between the column and the center
-     * column(s).
-     */
-    private float getColumnFactor(int col, int numCols) {
-        float centerColumn = numCols / 2;
-        int distanceToCenter = (int) Math.abs(col - centerColumn);
-
-        boolean evenNumberOfColumns = numCols % 2 == 0;
-        if (evenNumberOfColumns && col < centerColumn) {
-            distanceToCenter -= 1;
-        }
-
-        float factor = 0;
-        while (distanceToCenter > 0) {
-            if (distanceToCenter == 1) {
-                factor += 0.2f;
-            } else {
-                factor += 0.1f;
-            }
-            --distanceToCenter;
-        }
-
-        return factor;
+        return mVelocityDirection == Y_DIRECTION;
     }
 
     private VelocityTracker getVelocityTracker() {
@@ -249,4 +181,34 @@
         }
         return mVelocityTracker;
     }
+
+    /**
+     * This interface is used to initialize and update the SpringAnimations added to the
+     * {@link SpringAnimationHandler}.
+     *
+     * @param <T> The object that each SpringAnimation is attached to.
+     */
+    public interface AnimationFactory<T> {
+
+        /**
+         * Initializes a new Spring for {@param object}.
+         */
+        SpringAnimation initialize(T object);
+
+        /**
+         * Updates the value of {@param spring} based on {@param object}.
+         */
+        void update(SpringAnimation spring, T object);
+    }
+
+    /**
+     * Helper method to create a new SpringAnimation for {@param view}.
+     */
+    public static SpringAnimation forView(View view, FloatPropertyCompat property, float finalPos) {
+        SpringAnimation spring = new SpringAnimation(view, property, finalPos);
+        spring.setStartValue(1f);
+        spring.setSpring(new SpringForce(finalPos));
+        return spring;
+    }
+
 }