Merge "Enabling springs for start dismiss animation" into ub-launcher3-master
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
index 19a2bae..e500f0f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java
@@ -171,7 +171,8 @@
             }
         }
         anim.setDuration(accuracy);
-        mCurrentAnimation = AnimatorPlaybackController.wrap(anim, accuracy, this::clearState);
+        mCurrentAnimation = AnimatorPlaybackController.wrap(anim, accuracy)
+                .setOnCancelRunnable(this::clearState);
     }
 
     private void clearState() {
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index 715529e..52625dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -393,7 +393,7 @@
         xOverviewAnim.setFloatValues(startXProgress, endXProgress);
         xOverviewAnim.setDuration(xDuration)
                 .setInterpolator(scrollInterpolatorForVelocity(velocity.x));
-        mXOverviewAnim.dispatchOnStartWithVelocity(endXProgress, velocity.x);
+        mXOverviewAnim.dispatchOnStart();
 
         boolean flingUpToNormal = verticalFling && velocity.y < 0 && targetState == NORMAL;
 
@@ -414,7 +414,7 @@
         ValueAnimator yOverviewAnim = mYOverviewAnim.getAnimationPlayer();
         yOverviewAnim.setFloatValues(startYProgress, endYProgress);
         yOverviewAnim.setDuration(yDuration);
-        mYOverviewAnim.dispatchOnStartWithVelocity(endYProgress, velocity.y);
+        mYOverviewAnim.dispatchOnStart();
 
         ValueAnimator nonOverviewAnim = mNonOverviewAnim.getAnimationPlayer();
         if (flingUpToNormal && !mIsHomeScreenVisible) {
@@ -436,8 +436,7 @@
             float startProgress = mNonOverviewAnim.getProgressFraction();
             float endProgress = canceled ? 0 : 1;
             nonOverviewAnim.setFloatValues(startProgress, endProgress);
-            mNonOverviewAnim.dispatchOnStartWithVelocity(endProgress,
-                    horizontalFling ? velocity.x : velocity.y);
+            mNonOverviewAnim.dispatchOnStart();
         }
 
         nonOverviewAnim.setDuration(Math.max(xDuration, yDuration));
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
index 1f5228a..845699a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitOverviewStateTouchHelper.java
@@ -22,7 +22,7 @@
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.util.PendingAnimation;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index e0532ac..cc58fcf 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -16,17 +16,13 @@
 package com.android.launcher3.uioverrides.touchcontrollers;
 
 import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE;
 import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE;
-import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
 import android.view.MotionEvent;
 
 import com.android.launcher3.AbstractFloatingView;
@@ -35,12 +31,12 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
 import com.android.quickstep.SysUINavigationMode;
@@ -218,8 +214,8 @@
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setOnCancelRunnable(null);
         }
-        mCurrentAnimation = AnimatorPlaybackController.wrap(
-                mPendingAnimation.anim, maxDuration, this::clearState);
+        mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation, maxDuration)
+                .setOnCancelRunnable(this::clearState);
         onUserControlledAnimationCreated(mCurrentAnimation);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
@@ -288,26 +284,16 @@
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
         }
 
-        float nextFrameProgress = Utilities.boundToRange(progress
-                + velocity * getSingleFrameMs(mActivity) / Math.abs(mEndDisplacement), 0f, 1f);
-
         mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd, logAction));
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
-        anim.setDuration(animationDuration);
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
-            anim.addUpdateListener(valueAnimator -> {
+            mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> {
                 if (mRecentsView.getCurrentPage() != 0 || mCurrentAnimationIsGoingUp) {
                     mRecentsView.redrawLiveTile(true);
                 }
             });
         }
-        if (UNSTABLE_SPRINGS.get()) {
-            mCurrentAnimation.dispatchOnStartWithVelocity(goingToEnd ? 1f : 0f, velocity);
-        }
-        anim.start();
+        mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
+                velocity, mEndDisplacement, animationDuration);
     }
 
     private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
index 4b6241c..3328abc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -1000,7 +1000,7 @@
         mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
 
         if (UNSTABLE_SPRINGS.get()) {
-            mLauncherTransitionController.dispatchOnStartWithVelocity(end, velocityPxPerMs.y);
+            mLauncherTransitionController.dispatchOnStart();
         }
         mLauncherTransitionController.getAnimationPlayer().start();
         mHasLauncherTransitionControllerStarted = true;
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
index 62d6604..7eb3274 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/RecentsView.java
@@ -16,12 +16,9 @@
 
 package com.android.quickstep.views;
 
-import static androidx.dynamicanimation.animation.DynamicAnimation.MIN_VISIBLE_CHANGE_PIXELS;
-
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
 import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS;
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
 import static com.android.launcher3.LauncherState.BACKGROUND_APP;
 import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
 import static com.android.launcher3.Utilities.squaredHypot;
@@ -32,7 +29,6 @@
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.uioverrides.BackgroundBlurController.BACKGROUND_BLUR;
 import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS;
 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
@@ -46,7 +42,6 @@
 import android.animation.LayoutTransition;
 import android.animation.LayoutTransition.TransitionListener;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager;
@@ -96,8 +91,10 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.PendingAnimation.EndState;
 import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.SpringObjectAnimator;
+import com.android.launcher3.anim.SpringProperty;
 import com.android.launcher3.compat.AccessibilityManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragLayer;
@@ -111,7 +108,6 @@
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.OverScroller;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.ViewPool;
 import com.android.quickstep.RecentsAnimationController;
@@ -623,7 +619,7 @@
 
     protected void applyLoadPlan(ArrayList<Task> tasks) {
         if (mPendingAnimation != null) {
-            mPendingAnimation.addEndListener((onEndListener) -> applyLoadPlan(tasks));
+            mPendingAnimation.addEndListener((endState) -> applyLoadPlan(tasks));
             return;
         }
 
@@ -1210,37 +1206,29 @@
         }
     }
 
-    private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) {
-        addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
+    private void addDismissedTaskAnimations(View taskView, long duration, PendingAnimation anim) {
+        anim.add(ObjectAnimator.ofFloat(taskView, ALPHA, 0).setDuration(duration), ACCEL_2);
         FloatProperty<View> secondaryViewTranslate =
             mOrientationHandler.getSecondaryViewTranslate();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
         int verticalFactor = mOrientationHandler.getTaskDismissDirectionFactor();
-        if (UNSTABLE_SPRINGS.get() && taskView instanceof TaskView) {
-            ResourceProvider rp = DynamicResource.provider(mActivity);
-            float dampingRatio = rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio);
-            float stiffness = rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness);
 
-            addAnim(new SpringObjectAnimator<>(taskView, secondaryViewTranslate,
-                            MIN_VISIBLE_CHANGE_PIXELS, dampingRatio,
-                            stiffness, 0, verticalFactor * secondaryTaskDimension),
-                    duration, LINEAR, anim);
-        } else {
-            addAnim(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
-                verticalFactor * secondaryTaskDimension), duration, LINEAR, anim);
-        }
+        ResourceProvider rp = DynamicResource.provider(mActivity);
+        SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_START)
+                .setDampingRatio(rp.getFloat(R.dimen.dismiss_task_trans_y_damping_ratio))
+                .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_y_stiffness));
+
+        anim.add(ObjectAnimator.ofFloat(taskView, secondaryViewTranslate,
+                verticalFactor * secondaryTaskDimension).setDuration(duration), LINEAR, sp);
     }
 
-    private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener,
-                            boolean shouldLog) {
+    private void removeTask(Task task, int index, EndState endState) {
         if (task != null) {
             ActivityManagerWrapper.getInstance().removeTask(task.key.id);
-            if (shouldLog) {
-                ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key);
-                mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
-                        onEndListener.logAction, Direction.UP, index, componentKey);
-                mActivity.getStatsLogManager().logTaskDismiss(this, componentKey);
-            }
+            ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key);
+            mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
+                    endState.logAction, Direction.UP, index, componentKey);
+            mActivity.getStatsLogManager().logTaskDismiss(this, componentKey);
         }
     }
 
@@ -1249,12 +1237,11 @@
         if (mPendingAnimation != null) {
             mPendingAnimation.finish(false, Touch.SWIPE);
         }
-        AnimatorSet anim = new AnimatorSet();
-        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+        PendingAnimation anim = new PendingAnimation();
 
         int count = getPageCount();
         if (count == 0) {
-            return pendingAnimation;
+            return anim;
         }
 
         int[] oldScroll = new int[count];
@@ -1273,7 +1260,7 @@
             View child = getChildAt(i);
             if (child == taskView) {
                 if (animateTaskView) {
-                    addDismissedTaskAnimations(taskView, anim, duration);
+                    addDismissedTaskAnimations(taskView, duration, anim);
                 }
             } else {
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
@@ -1297,20 +1284,15 @@
                 }
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
-                    if (UNSTABLE_SPRINGS.get() && child instanceof TaskView) {
-                        ResourceProvider rp = DynamicResource.provider(mActivity);
-                        float damping = rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio);
-                        float stiffness = rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness);
+                    Property translationProperty = mOrientationHandler.getPrimaryViewTranslate();
 
-                        addAnim(new SpringObjectAnimator<>(child, VIEW_TRANSLATE_X,
-                                MIN_VISIBLE_CHANGE_PIXELS, damping,
-                                stiffness, 0, scrollDiff), duration, ACCEL, anim);
-                    } else {
-                        Property translationProperty = mOrientationHandler.getPrimaryViewTranslate();
-                        addAnim(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff),
-                            duration, ACCEL, anim);
-                    }
-
+                    ResourceProvider rp = DynamicResource.provider(mActivity);
+                    SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
+                            .setDampingRatio(
+                                    rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
+                            .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
+                    anim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
+                            .setDuration(duration), ACCEL, sp);
                     needsCurveUpdates = true;
                 }
             }
@@ -1319,7 +1301,7 @@
         if (needsCurveUpdates) {
             ValueAnimator va = ValueAnimator.ofFloat(0, 1);
             va.addUpdateListener((a) -> updateCurveProperties());
-            anim.play(va);
+            anim.add(va);
         }
 
         // Add a tiny bit of translation Z, so that it draws on top of other views
@@ -1327,22 +1309,22 @@
             taskView.setTranslationZ(0.1f);
         }
 
-        mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener(new Consumer<PendingAnimation.OnEndListener>() {
+        mPendingAnimation = anim;
+        mPendingAnimation.addEndListener(new Consumer<EndState>() {
             @Override
-            public void accept(PendingAnimation.OnEndListener onEndListener) {
+            public void accept(EndState endState) {
                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() &&
-                        taskView.isRunningTask() && onEndListener.isSuccess) {
-                    finishRecentsAnimation(true /* toHome */, () -> onEnd(onEndListener));
+                        taskView.isRunningTask() && endState.isSuccess) {
+                    finishRecentsAnimation(true /* toHome */, () -> onEnd(endState));
                 } else {
-                    onEnd(onEndListener);
+                    onEnd(endState);
                 }
             }
 
-            private void onEnd(PendingAnimation.OnEndListener onEndListener) {
-                if (onEndListener.isSuccess) {
+            private void onEnd(EndState endState) {
+                if (endState.isSuccess) {
                     if (shouldRemoveTask) {
-                        removeTask(taskView.getTask(), draggedIndex, onEndListener, true);
+                        removeTask(taskView.getTask(), draggedIndex, endState);
                     }
 
                     int pageToSnapTo = mCurrentPage;
@@ -1364,24 +1346,23 @@
                 mPendingAnimation = null;
             }
         });
-        return pendingAnimation;
+        return anim;
     }
 
     public PendingAnimation createAllTasksDismissAnimation(long duration) {
         if (FeatureFlags.IS_STUDIO_BUILD && mPendingAnimation != null) {
             throw new IllegalStateException("Another pending animation is still running");
         }
-        AnimatorSet anim = new AnimatorSet();
-        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+        PendingAnimation anim = new PendingAnimation();
 
         int count = getTaskViewCount();
         for (int i = 0; i < count; i++) {
-            addDismissedTaskAnimations(getTaskViewAt(i), anim, duration);
+            addDismissedTaskAnimations(getTaskViewAt(i), duration, anim);
         }
 
-        mPendingAnimation = pendingAnimation;
-        mPendingAnimation.addEndListener((onEndListener) -> {
-            if (onEndListener.isSuccess) {
+        mPendingAnimation = anim;
+        mPendingAnimation.addEndListener((endState) -> {
+            if (endState.isSuccess) {
                 // Remove all the task views now
                 ActivityManagerWrapper.getInstance().removeAllRecentTasks();
                 removeTasksViewsAndClearAllButton();
@@ -1389,13 +1370,7 @@
             }
             mPendingAnimation = null;
         });
-        return pendingAnimation;
-    }
-
-    private static void addAnim(Animator anim, long duration,
-            TimeInterpolator interpolator, AnimatorSet set) {
-        anim.setDuration(duration).setInterpolator(interpolator);
-        set.play(anim);
+        return anim;
     }
 
     private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
@@ -1412,8 +1387,8 @@
     }
 
     private void runDismissAnimation(PendingAnimation pendingAnim) {
-        AnimatorPlaybackController controller = AnimatorPlaybackController.wrap(
-                pendingAnim.anim, DISMISS_TASK_DURATION);
+        AnimatorPlaybackController controller =
+                AnimatorPlaybackController.wrap(pendingAnim, DISMISS_TASK_DURATION);
         controller.dispatchOnStart();
         controller.setEndAction(() -> pendingAnim.finish(true, Touch.SWIPE));
         controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN);
@@ -1738,7 +1713,7 @@
 
         int count = getTaskViewCount();
         if (count == 0) {
-            return new PendingAnimation(new AnimatorSet());
+            return new PendingAnimation();
         }
 
         int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags();
@@ -1780,13 +1755,12 @@
         anim.setDuration(duration)
                 .setInterpolator(interpolator);
 
-        Consumer<Boolean> onTaskLaunchFinish = this::onTaskLaunched;
-
-        mPendingAnimation = new PendingAnimation(anim);
-        mPendingAnimation.addEndListener((onEndListener) -> {
-            if (onEndListener.isSuccess) {
+        mPendingAnimation = new PendingAnimation();
+        mPendingAnimation.add(anim);
+        mPendingAnimation.addEndListener((endState) -> {
+            if (endState.isSuccess) {
                 Consumer<Boolean> onLaunchResult = (result) -> {
-                    onTaskLaunchFinish.accept(result);
+                    onTaskLaunched(result);
                     if (!result) {
                         tv.notifyTaskLaunchFailed(TAG);
                     }
@@ -1795,11 +1769,11 @@
                 Task task = tv.getTask();
                 if (task != null) {
                     mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(
-                            onEndListener.logAction, Direction.DOWN, indexOfChild(tv),
+                            endState.logAction, Direction.DOWN, indexOfChild(tv),
                             TaskUtils.getLaunchComponentKeyForTask(task.key));
                 }
             } else {
-                onTaskLaunchFinish.accept(false);
+                onTaskLaunched(false);
             }
             mPendingAnimation = null;
         });
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
index e09e01f..abc037e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskView.java
@@ -58,6 +58,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.states.RotationHelper;
@@ -67,7 +68,6 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.ViewPool.Reusable;
 import com.android.quickstep.RecentsModel;
 import com.android.quickstep.TaskIconCache;
@@ -278,8 +278,8 @@
     public AnimatorPlaybackController createLaunchAnimationForRunningTask() {
         final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation(
                 this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR);
-        AnimatorPlaybackController currentAnimation = AnimatorPlaybackController.wrap(
-                pendingAnimation.anim, RECENTS_LAUNCH_DURATION);
+        AnimatorPlaybackController currentAnimation =
+                AnimatorPlaybackController.wrap(pendingAnimation, RECENTS_LAUNCH_DURATION);
         currentAnimation.setEndAction(() -> {
             pendingAnimation.finish(true, Touch.SWIPE);
             launchTask(false);
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index fe830d2..95e38e3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -87,8 +87,7 @@
     protected boolean canInterceptTouch(MotionEvent ev) {
         if (mCurrentAnimation != null) {
             if (mFinishFastOnSecondTouch) {
-                // TODO: Animate to finish instead.
-                mCurrentAnimation.skipToEnd();
+                mCurrentAnimation.getAnimationPlayer().end();
             }
 
             AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
@@ -233,8 +232,8 @@
                 cancelPendingAnim();
                 clearState();
             };
-            mCurrentAnimation = AnimatorPlaybackController.wrap(
-                    mPendingAnimation.anim, maxAccuracy, onCancelRunnable);
+            mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation, maxAccuracy)
+                    .setOnCancelRunnable(onCancelRunnable);
             mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation);
             totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher,
                     mLauncher.getDeviceProfile());
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index 9f25729..a8f2025 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -376,8 +376,8 @@
         mConfig.animComponents = animComponents;
         mConfig.duration = duration;
         mConfig.playbackController = AnimatorPlaybackController.wrap(
-                createAnimationToNewWorkspaceInternal(state, builder, null), duration,
-                onCancelRunnable);
+                createAnimationToNewWorkspaceInternal(state, builder, null), duration)
+                .setOnCancelRunnable(onCancelRunnable);
         return mConfig.playbackController;
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index 2179162..32c65a3 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -12,7 +12,6 @@
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
-import static com.android.launcher3.config.FeatureFlags.UNSTABLE_SPRINGS;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
 
 import android.animation.Animator;
@@ -31,11 +30,8 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.PropertySetter;
-import com.android.launcher3.anim.SpringObjectAnimator;
-import com.android.launcher3.util.DynamicResource;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.ScrimView;
-import com.android.systemui.plugins.ResourceProvider;
 
 /**
  * Handles AllApps view transition.
@@ -185,14 +181,6 @@
     }
 
     public Animator createSpringAnimation(float... progressValues) {
-        if (UNSTABLE_SPRINGS.get()) {
-            ResourceProvider rp = DynamicResource.provider(mLauncher);
-            float damping = rp.getFloat(R.dimen.all_apps_spring_damping_ratio);
-            float stiffness = rp.getFloat(R.dimen.all_apps_spring_stiffness);
-
-            return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange,
-                    damping, stiffness, progressValues);
-        }
         return ObjectAnimator.ofFloat(this, ALL_APPS_PROGRESS, progressValues);
     }
 
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 1c277ab..1fc21fd 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -16,7 +16,9 @@
 package com.android.launcher3.anim;
 
 import static com.android.launcher3.anim.Interpolators.LINEAR;
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
 
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
@@ -24,17 +26,18 @@
 import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
-import android.util.Log;
+import android.content.Context;
+import android.util.FloatProperty;
 
 import androidx.annotation.Nullable;
-import androidx.dynamicanimation.animation.DynamicAnimation;
-import androidx.dynamicanimation.animation.SpringAnimation;
+
+import com.android.launcher3.Utilities;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 /**
  * Helper class to control the playback of an {@link AnimatorSet}, with custom interpolators
@@ -43,14 +46,7 @@
  * Note: The implementation does not support start delays on child animations or
  * sequential playbacks.
  */
-public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
-
-    private static final String TAG = "AnimatorPlaybackCtrler";
-    private static boolean DEBUG = false;
-
-    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
-        return wrap(anim, duration, null);
-    }
+public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
     /**
      * Creates an animation controller for the provided animation.
@@ -58,20 +54,41 @@
      * needs to be larger than the total number of pixels so that we don't have jittering due
      * to float (animation-fraction * total duration) to int conversion.
      */
-    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration,
-            Runnable onCancelRunnable) {
-
+    public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
         /**
          * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
          */
-        return new AnimatorPlaybackControllerVL(anim, duration, onCancelRunnable);
+        ArrayList<Holder> childAnims = new ArrayList<>();
+        addAnimationHoldersRecur(anim, SpringProperty.DEFAULT, childAnims);
+
+        return new AnimatorPlaybackController(anim, duration, childAnims);
     }
 
+    public static AnimatorPlaybackController wrap(PendingAnimation anim, long duration) {
+        /**
+         * TODO: use {@link AnimatorSet#setCurrentPlayTime(long)} once b/68382377 is fixed.
+         */
+        return new AnimatorPlaybackController(anim.anim, duration, anim.animHolders);
+    }
+
+    private static final FloatProperty<ValueAnimator> CURRENT_PLAY_TIME =
+            new FloatProperty<ValueAnimator>("current-play-time") {
+                @Override
+                public void setValue(ValueAnimator animator, float v) {
+                    animator.setCurrentPlayTime((long) v);
+                }
+
+                @Override
+                public Float get(ValueAnimator animator) {
+                    return (float) animator.getCurrentPlayTime();
+                }
+            };
+
     private final ValueAnimator mAnimationPlayer;
     private final long mDuration;
 
-    protected final AnimatorSet mAnim;
-    private Set<SpringAnimation> mSprings;
+    private final AnimatorSet mAnim;
+    private final Holder[] mChildAnimations;
 
     protected float mCurrentFraction;
     private Runnable mEndAction;
@@ -79,22 +96,14 @@
     protected boolean mTargetCancelled = false;
     protected Runnable mOnCancelRunnable;
 
-    private OnAnimationEndDispatcher mEndListener;
-    private DynamicAnimation.OnAnimationEndListener mSpringEndListener;
-    // We need this variable to ensure the end listener is called immediately, otherwise we run into
-    // issues where the callback interferes with the states of the swipe detector.
-    private boolean mSkipToEnd = false;
-
-    protected AnimatorPlaybackController(AnimatorSet anim, long duration,
-            Runnable onCancelRunnable) {
+    private AnimatorPlaybackController(
+            AnimatorSet anim, long duration, ArrayList<Holder> childAnims) {
         mAnim = anim;
         mDuration = duration;
-        mOnCancelRunnable = onCancelRunnable;
 
         mAnimationPlayer = ValueAnimator.ofFloat(0, 1);
         mAnimationPlayer.setInterpolator(LINEAR);
-        mEndListener = new OnAnimationEndDispatcher();
-        mAnimationPlayer.addListener(mEndListener);
+        mAnimationPlayer.addListener(new OnAnimationEndDispatcher());
         mAnimationPlayer.addUpdateListener(this);
 
         mAnim.addListener(new AnimatorListenerAdapter() {
@@ -119,14 +128,7 @@
             }
         });
 
-        mSprings = new HashSet<>();
-        mSpringEndListener = (animation, canceled, value, velocity1) -> {
-            if (canceled) {
-                mEndListener.onAnimationCancel(mAnimationPlayer);
-            } else {
-                mEndListener.onAnimationEnd(mAnimationPlayer);
-            }
-        };
+        mChildAnimations = childAnims.toArray(new Holder[childAnims.size()]);
     }
 
     public AnimatorSet getTarget() {
@@ -160,9 +162,68 @@
     }
 
     /**
+     * Starts playing the animation with the provided velocity optionally playing any
+     * physics based animations
+     */
+    public void startWithVelocity(Context context, boolean goingToEnd,
+            float velocity, float scale, long animationDuration) {
+        float scaleInverse = 1 / Math.abs(scale);
+        float scaledVelocity = velocity * scaleInverse;
+
+        float nextFrameProgress = Utilities.boundToRange(getProgressFraction()
+                + scaledVelocity * getSingleFrameMs(context), 0f, 1f);
+
+        // Update setters for spring
+        int springFlag = goingToEnd
+                ? SpringProperty.FLAG_CAN_SPRING_ON_END
+                : SpringProperty.FLAG_CAN_SPRING_ON_START;
+
+        long springDuration = animationDuration;
+        for (Holder h : mChildAnimations) {
+            if ((h.springProperty.flags & springFlag) != 0) {
+                SpringAnimationBuilder s = new SpringAnimationBuilder(h.anim, CURRENT_PLAY_TIME)
+                        .setStartValue(clampDuration(mCurrentFraction))
+                        .setEndValue(goingToEnd ? h.anim.getDuration() : 0)
+                        .setStartVelocity(scaledVelocity * h.anim.getDuration())
+                        .setMinimumVisibleChange(scaleInverse)
+                        .setDampingRatio(h.springProperty.mDampingRatio)
+                        .setStiffness(h.springProperty.mStiffness);
+
+                long expectedDurationL = s.build(context).getDuration();
+                springDuration = Math.max(expectedDurationL, springDuration);
+
+                float expectedDuration = expectedDurationL;
+                h.setter = (a, l) ->
+                    s.setValue(a, mAnimationPlayer.getCurrentPlayTime() / expectedDuration);
+                h.anim.setInterpolator(LINEAR);
+            }
+        }
+
+        mAnimationPlayer.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
+
+        if (springDuration <= animationDuration) {
+            mAnimationPlayer.setDuration(animationDuration);
+            mAnimationPlayer.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        } else {
+            // Since spring requires more time to run, we let the other animations play with
+            // current time and interpolation and by clamping the duration.
+            mAnimationPlayer.setDuration(springDuration);
+
+            float cutOff = animationDuration / (float) springDuration;
+            mAnimationPlayer.setInterpolator(
+                    clampToProgress(scrollInterpolatorForVelocity(velocity), 0, cutOff));
+        }
+        mAnimationPlayer.start();
+    }
+
+    /**
      * Pauses the currently playing animation.
      */
     public void pause() {
+        // Reset property setters
+        for (Holder h : mChildAnimations) {
+            h.reset();
+        }
         mAnimationPlayer.cancel();
     }
 
@@ -176,7 +237,18 @@
     /**
      * Sets the current animation position and updates all the child animators accordingly.
      */
-    public abstract void setPlayFraction(float fraction);
+    public void setPlayFraction(float fraction) {
+        mCurrentFraction = fraction;
+        // Let the animator report the progress but don't apply the progress to child
+        // animations if it has been cancelled.
+        if (mTargetCancelled) {
+            return;
+        }
+        long playPos = clampDuration(fraction);
+        for (Holder holder : mChildAnimations) {
+            holder.setter.set(holder.anim, playPos);
+        }
+    }
 
     public float getProgressFraction() {
         return mCurrentFraction;
@@ -208,49 +280,6 @@
         }
     }
 
-    /**
-     * Starts playback and sets the spring.
-     */
-    public void dispatchOnStartWithVelocity(float end, float velocity) {
-        if (!QUICKSTEP_SPRINGS.get()) {
-            dispatchOnStart();
-            return;
-        }
-
-        if (DEBUG) Log.d(TAG, "dispatchOnStartWithVelocity#end=" + end + ", velocity=" + velocity);
-
-        for (Animator a : mAnim.getChildAnimations()) {
-            if (a instanceof SpringObjectAnimator) {
-                if (DEBUG) Log.d(TAG, "Found springAnimator=" + a);
-                SpringObjectAnimator springAnimator = (SpringObjectAnimator) a;
-                mSprings.add(springAnimator.getSpring());
-                springAnimator.startSpring(end, velocity, mSpringEndListener);
-            }
-        }
-
-        dispatchOnStart();
-    }
-
-    public void dispatchOnStart() {
-        dispatchOnStartRecursively(mAnim);
-    }
-
-    private void dispatchOnStartRecursively(Animator animator) {
-        List<AnimatorListener> listeners = animator instanceof SpringObjectAnimator
-                ? nonNullList(((SpringObjectAnimator) animator).getObjectAnimatorListeners())
-                : nonNullList(animator.getListeners());
-
-        for (AnimatorListener l : listeners) {
-            l.onAnimationStart(animator);
-        }
-
-        if (animator instanceof AnimatorSet) {
-            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                dispatchOnStartRecursively(anim);
-            }
-        }
-    }
-
     /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */
     public void dispatchOnCancelWithoutCancelRunnable() {
         dispatchOnCancelWithoutCancelRunnable(null);
@@ -272,115 +301,47 @@
         setOnCancelRunnable(onCancel);
     }
 
-    public void dispatchOnCancel() {
-        dispatchOnCancelRecursively(mAnim);
+
+    public AnimatorPlaybackController setOnCancelRunnable(Runnable runnable) {
+        mOnCancelRunnable = runnable;
+        return this;
     }
 
-    private void dispatchOnCancelRecursively(Animator animator) {
-        for (AnimatorListener l : nonNullList(animator.getListeners())) {
-            l.onAnimationCancel(animator);
-        }
+    public void dispatchOnStart() {
+        callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart);
+    }
 
-        if (animator instanceof AnimatorSet) {
-            for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                dispatchOnCancelRecursively(anim);
-            }
-        }
+    public void dispatchOnCancel() {
+        callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationCancel);
     }
 
     public void dispatchSetInterpolator(TimeInterpolator interpolator) {
-        dispatchSetInterpolatorRecursively(mAnim, interpolator);
+        callAnimatorCommandRecursively(mAnim, a -> a.setInterpolator(interpolator));
     }
 
-    private void dispatchSetInterpolatorRecursively(Animator anim, TimeInterpolator interpolator) {
-        anim.setInterpolator(interpolator);
+    private static void callListenerCommandRecursively(
+            Animator anim, BiConsumer<AnimatorListener, Animator> command) {
+        callAnimatorCommandRecursively(anim, a-> {
+            for (AnimatorListener l : nonNullList(a.getListeners())) {
+                command.accept(l, a);
+            }
+        });
+    }
+
+    private static void callAnimatorCommandRecursively(Animator anim, Consumer<Animator> command) {
+        command.accept(anim);
         if (anim instanceof AnimatorSet) {
             for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) {
-                dispatchSetInterpolatorRecursively(child, interpolator);
+                callAnimatorCommandRecursively(child, command);
             }
         }
     }
 
-    public void setOnCancelRunnable(Runnable runnable) {
-        mOnCancelRunnable = runnable;
-    }
-
-    public void skipToEnd() {
-        mSkipToEnd = true;
-        for (SpringAnimation spring : mSprings) {
-            if (spring.canSkipToEnd()) {
-                spring.skipToEnd();
-            }
-        }
-        mAnimationPlayer.end();
-        mSkipToEnd = false;
-    }
-
-    public static class AnimatorPlaybackControllerVL extends AnimatorPlaybackController {
-
-        private final ValueAnimator[] mChildAnimations;
-
-        private AnimatorPlaybackControllerVL(AnimatorSet anim, long duration,
-                Runnable onCancelRunnable) {
-            super(anim, duration, onCancelRunnable);
-
-            // Build animation list
-            ArrayList<ValueAnimator> childAnims = new ArrayList<>();
-            getAnimationsRecur(mAnim, childAnims);
-            mChildAnimations = childAnims.toArray(new ValueAnimator[childAnims.size()]);
-        }
-
-        private void getAnimationsRecur(AnimatorSet anim, ArrayList<ValueAnimator> out) {
-            long forceDuration = anim.getDuration();
-            TimeInterpolator forceInterpolator = anim.getInterpolator();
-            for (Animator child : anim.getChildAnimations()) {
-                if (forceDuration > 0) {
-                    child.setDuration(forceDuration);
-                }
-                if (forceInterpolator != null) {
-                    child.setInterpolator(forceInterpolator);
-                }
-                if (child instanceof ValueAnimator) {
-                    out.add((ValueAnimator) child);
-                } else if (child instanceof AnimatorSet) {
-                    getAnimationsRecur((AnimatorSet) child, out);
-                } else {
-                    throw new RuntimeException("Unknown animation type " + child);
-                }
-            }
-        }
-
-        @Override
-        public void setPlayFraction(float fraction) {
-            mCurrentFraction = fraction;
-            // Let the animator report the progress but don't apply the progress to child
-            // animations if it has been cancelled.
-            if (mTargetCancelled) {
-                return;
-            }
-            long playPos = clampDuration(fraction);
-            for (ValueAnimator anim : mChildAnimations) {
-                anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration()));
-            }
-        }
-    }
-
-    private boolean isAnySpringRunning() {
-        for (SpringAnimation spring : mSprings) {
-            if (spring.isRunning()) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /**
      * Only dispatches the on end actions once the animator and all springs have completed running.
      */
     private class OnAnimationEndDispatcher extends AnimationSuccessListener {
 
-        boolean mAnimatorDone = false;
-        boolean mSpringsDone = false;
         boolean mDispatched = false;
 
         @Override
@@ -391,39 +352,76 @@
 
         @Override
         public void onAnimationSuccess(Animator animator) {
-            if (mSprings.isEmpty()) {
-                mSpringsDone = mAnimatorDone = true;
-            }
-            if (isAnySpringRunning()) {
-                mAnimatorDone = true;
-            } else {
-                mSpringsDone = true;
-            }
-
             // We wait for the spring (if any) to finish running before completing the end callback.
-            if (!mDispatched && (mSkipToEnd || (mAnimatorDone && mSpringsDone))) {
-                dispatchOnEndRecursively(mAnim);
+            if (!mDispatched) {
+                callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationEnd);
                 if (mEndAction != null) {
                     mEndAction.run();
                 }
                 mDispatched = true;
             }
         }
-
-        private void dispatchOnEndRecursively(Animator animator) {
-            for (AnimatorListener l : nonNullList(animator.getListeners())) {
-                l.onAnimationEnd(animator);
-            }
-
-            if (animator instanceof AnimatorSet) {
-                for (Animator anim : nonNullList(((AnimatorSet) animator).getChildAnimations())) {
-                    dispatchOnEndRecursively(anim);
-                }
-            }
-        }
     }
 
     private static <T> List<T> nonNullList(ArrayList<T> list) {
         return list == null ? Collections.emptyList() : list;
     }
+
+    /**
+     * Interface for setting position of value animator
+     */
+    private interface PositionSetter {
+
+        PositionSetter DEFAULT = (anim, playPos) ->
+                anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration()));
+
+        void set(ValueAnimator anim, long position);
+    }
+
+    /**
+     * Holder class for various child animations
+     */
+    static class Holder {
+
+        public final ValueAnimator anim;
+
+        public final SpringProperty springProperty;
+
+        public final TimeInterpolator interpolator;
+
+        public PositionSetter setter;
+
+        Holder(Animator anim, SpringProperty springProperty) {
+            this.anim = (ValueAnimator) anim;
+            this.springProperty = springProperty;
+            this.interpolator = this.anim.getInterpolator();
+            this.setter = PositionSetter.DEFAULT;
+        }
+
+        public void reset() {
+            anim.setInterpolator(interpolator);
+            setter = PositionSetter.DEFAULT;
+        }
+    }
+
+    static void addAnimationHoldersRecur(
+            Animator anim, SpringProperty springProperty, ArrayList<Holder> out) {
+        long forceDuration = anim.getDuration();
+        TimeInterpolator forceInterpolator = anim.getInterpolator();
+        if (anim instanceof ValueAnimator) {
+            out.add(new Holder(anim, springProperty));
+        } else if (anim instanceof AnimatorSet) {
+            for (Animator child : ((AnimatorSet) anim).getChildAnimations()) {
+                if (forceDuration > 0) {
+                    child.setDuration(forceDuration);
+                }
+                if (forceInterpolator != null) {
+                    child.setInterpolator(forceInterpolator);
+                }
+                addAnimationHoldersRecur(child, springProperty, out);
+            }
+        } else {
+            throw new RuntimeException("Unknown animation type " + anim);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java
new file mode 100644
index 0000000..562d160
--- /dev/null
+++ b/src/com/android/launcher3/anim/PendingAnimation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.anim;
+
+import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.launcher3.anim.AnimatorPlaybackController.Holder;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to keep track of a running animation.
+ *
+ * This class allows attaching end callbacks to an animation is intended to be used with
+ * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case
+ * AnimationListeners are not properly dispatched.
+ *
+ * TODO: Find a better name
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class PendingAnimation {
+
+    private final ArrayList<Consumer<EndState>> mEndListeners = new ArrayList<>();
+
+    /** package private **/
+    final AnimatorSet anim = new AnimatorSet();
+    final ArrayList<Holder> animHolders = new ArrayList<>();
+
+    /**
+     * Utility method to sent an interpolator on an animation and add it to the list
+     */
+    public void add(Animator anim, TimeInterpolator interpolator) {
+        add(anim, interpolator, SpringProperty.DEFAULT);
+    }
+
+    public void add(Animator anim, TimeInterpolator interpolator, SpringProperty springProperty) {
+        anim.setInterpolator(interpolator);
+        add(anim, springProperty);
+    }
+
+    public void add(Animator anim) {
+        add(anim, SpringProperty.DEFAULT);
+    }
+
+    public void add(Animator a, SpringProperty springProperty) {
+        anim.play(a);
+        addAnimationHoldersRecur(a, springProperty, animHolders);
+    }
+
+    public void finish(boolean isSuccess, int logAction) {
+        for (Consumer<EndState> listeners : mEndListeners) {
+            listeners.accept(new EndState(isSuccess, logAction));
+        }
+        mEndListeners.clear();
+    }
+
+    public void addEndListener(Consumer<EndState> listener) {
+        mEndListeners.add(listener);
+    }
+
+    public static class EndState {
+        public boolean isSuccess;
+        public int logAction;
+
+        public EndState(boolean isSuccess, int logAction) {
+            this.isSuccess = isSuccess;
+            this.logAction = logAction;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/anim/SpringObjectAnimator.java b/src/com/android/launcher3/anim/SpringObjectAnimator.java
deleted file mode 100644
index 27b9c18..0000000
--- a/src/com/android/launcher3/anim/SpringObjectAnimator.java
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.anim;
-
-import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat;
-
-import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.os.Handler;
-import android.os.Looper;
-import android.util.FloatProperty;
-import android.util.Log;
-
-import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
-import androidx.dynamicanimation.animation.SpringAnimation;
-import androidx.dynamicanimation.animation.SpringForce;
-
-import java.util.ArrayList;
-
-
-/**
- * This animator allows for an object's property to be be controlled by an {@link ObjectAnimator} or
- * a {@link SpringAnimation}. It extends ValueAnimator so it can be used in an AnimatorSet.
- */
-public class SpringObjectAnimator<T> extends ValueAnimator {
-
-    private static final String TAG = "SpringObjectAnimator";
-    private static boolean DEBUG = false;
-
-    private ObjectAnimator mObjectAnimator;
-    private float[] mValues;
-
-    private SpringAnimation mSpring;
-    private SpringProperty<T> mProperty;
-
-    private ArrayList<AnimatorListener> mListeners;
-    private boolean mSpringEnded = true;
-    private boolean mAnimatorEnded = true;
-    private boolean mEnded = true;
-
-    public SpringObjectAnimator(T object, FloatProperty<T> property, float minimumVisibleChange,
-            float damping, float stiffness, float... values) {
-        mSpring = new SpringAnimation(object, createFloatPropertyCompat(property));
-        mSpring.setMinimumVisibleChange(minimumVisibleChange);
-        mSpring.setSpring(new SpringForce(0)
-                .setDampingRatio(damping)
-                .setStiffness(stiffness));
-        mSpring.setStartVelocity(0.01f);
-        mProperty = new SpringProperty<>(property, mSpring);
-        mObjectAnimator = ObjectAnimator.ofFloat(object, mProperty, values);
-        mValues = values;
-        mListeners = new ArrayList<>();
-        setFloatValues(values);
-
-        // We use this listener and track mListeners so that we can sync the animator and spring
-        // listeners.
-        mObjectAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                mAnimatorEnded = false;
-                mEnded = false;
-                for (AnimatorListener l : mListeners) {
-                    l.onAnimationStart(animation);
-                }
-            }
-
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mAnimatorEnded = true;
-                tryEnding();
-            }
-
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                for (AnimatorListener l : mListeners) {
-                    l.onAnimationCancel(animation);
-                }
-                mSpring.cancel();
-            }
-        });
-
-        mSpring.addUpdateListener((animation, value, velocity) -> {
-            mSpringEnded = false;
-            mEnded = false;
-        });
-        mSpring.addEndListener((animation, canceled, value, velocity) -> {
-            mSpringEnded = true;
-            tryEnding();
-        });
-    }
-
-    private void tryEnding() {
-        if (DEBUG) {
-            Log.d(TAG, "tryEnding#mAnimatorEnded=" + mAnimatorEnded + ", mSpringEnded="
-                    + mSpringEnded + ", mEnded=" + mEnded);
-        }
-
-        // If springs are disabled, ignore value of mSpringEnded
-        if (mAnimatorEnded && (mSpringEnded || !QUICKSTEP_SPRINGS.get()) && !mEnded) {
-            for (AnimatorListener l : mListeners) {
-                l.onAnimationEnd(this);
-            }
-            mEnded = true;
-        }
-    }
-
-    public SpringAnimation getSpring() {
-        return mSpring;
-    }
-
-    /**
-     * Initializes and sets up the spring to take over controlling the object.
-     */
-    public void startSpring(float end, float velocity, OnAnimationEndListener endListener) {
-        // Cancel the spring so we can set new start velocity and final position. We need to remove
-        // the listener since the spring is not actually ending.
-        mSpring.removeEndListener(endListener);
-        mSpring.cancel();
-        mSpring.addEndListener(endListener);
-
-        mProperty.switchToSpring();
-
-        float startValue = end == 0 ? mValues[1] : mValues[0];
-        float endValue = end == 0 ? mValues[0] : mValues[1];
-
-        // Ensures that the velocity matches the direction of the values.
-        velocity = Math.signum(endValue - startValue) * Math.abs(velocity);
-        mSpring.setStartVelocity(velocity);
-
-        new Handler(Looper.getMainLooper()).postDelayed(() -> {
-            mSpring.animateToFinalPosition(endValue);
-        }, getStartDelay());
-    }
-
-    @Override
-    public void addListener(AnimatorListener listener) {
-        mListeners.add(listener);
-    }
-
-    public ArrayList<AnimatorListener> getObjectAnimatorListeners() {
-        return mObjectAnimator.getListeners();
-    }
-
-    @Override
-    public ArrayList<AnimatorListener> getListeners() {
-        return mListeners;
-    }
-
-    @Override
-    public void removeAllListeners() {
-        mListeners.clear();
-    }
-
-    @Override
-    public void removeListener(AnimatorListener listener) {
-        mListeners.remove(listener);
-    }
-
-    @Override
-    public void addPauseListener(AnimatorPauseListener listener) {
-        mObjectAnimator.addPauseListener(listener);
-    }
-
-    @Override
-    public void cancel() {
-        mObjectAnimator.cancel();
-        mSpring.cancel();
-    }
-
-    @Override
-    public void end() {
-        mObjectAnimator.end();
-    }
-
-    @Override
-    public long getDuration() {
-        return mObjectAnimator.getDuration();
-    }
-
-    @Override
-    public TimeInterpolator getInterpolator() {
-        return mObjectAnimator.getInterpolator();
-    }
-
-    @Override
-    public long getStartDelay() {
-        return mObjectAnimator.getStartDelay();
-    }
-
-    @Override
-    public long getTotalDuration() {
-        return mObjectAnimator.getTotalDuration();
-    }
-
-    @Override
-    public boolean isPaused() {
-        return mObjectAnimator.isPaused();
-    }
-
-    @Override
-    public boolean isRunning() {
-        return mObjectAnimator.isRunning();
-    }
-
-    @Override
-    public boolean isStarted() {
-        return mObjectAnimator.isStarted();
-    }
-
-    @Override
-    public void pause() {
-        mObjectAnimator.pause();
-    }
-
-    @Override
-    public void removePauseListener(AnimatorPauseListener listener) {
-        mObjectAnimator.removePauseListener(listener);
-    }
-
-    @Override
-    public void resume() {
-        mObjectAnimator.resume();
-    }
-
-    @Override
-    public ValueAnimator setDuration(long duration) {
-        return mObjectAnimator.setDuration(duration);
-    }
-
-    @Override
-    public void setInterpolator(TimeInterpolator value) {
-        mObjectAnimator.setInterpolator(value);
-    }
-
-    @Override
-    public void setStartDelay(long startDelay) {
-        mObjectAnimator.setStartDelay(startDelay);
-    }
-
-    @Override
-    public void setTarget(Object target) {
-        mObjectAnimator.setTarget(target);
-    }
-
-    @Override
-    public void start() {
-        mObjectAnimator.start();
-    }
-
-    @Override
-    public void setCurrentFraction(float fraction) {
-        mObjectAnimator.setCurrentFraction(fraction);
-    }
-
-    @Override
-    public void setCurrentPlayTime(long playTime) {
-        mObjectAnimator.setCurrentPlayTime(playTime);
-    }
-
-    public static class SpringProperty<T> extends FloatProperty<T> {
-
-        boolean useSpring = false;
-        final FloatProperty<T> mProperty;
-        final SpringAnimation mSpring;
-
-        public SpringProperty(FloatProperty<T> property, SpringAnimation spring) {
-            super(property.getName());
-            mProperty = property;
-            mSpring = spring;
-        }
-
-        public void switchToSpring() {
-            useSpring = true;
-        }
-
-        @Override
-        public Float get(T object) {
-            return mProperty.get(object);
-        }
-
-        @Override
-        public void setValue(T object, float progress) {
-            if (useSpring) {
-                mSpring.animateToFinalPosition(progress);
-            } else {
-                mProperty.setValue(object, progress);
-            }
-        }
-    }
-}
diff --git a/src/com/android/launcher3/anim/SpringProperty.java b/src/com/android/launcher3/anim/SpringProperty.java
new file mode 100644
index 0000000..caedd6c
--- /dev/null
+++ b/src/com/android/launcher3/anim/SpringProperty.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.anim;
+
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * Utility class to store configurations for spring animation
+ */
+public class SpringProperty {
+
+    public static final SpringProperty DEFAULT = new SpringProperty();
+
+    // Play spring when the animation is going towards the end
+    public static final int FLAG_CAN_SPRING_ON_END = 1 << 0;
+    // Play spring when animation is going towards the start (in reverse direction)
+    public static final int FLAG_CAN_SPRING_ON_START = 1 << 1;
+
+    public final int flags;
+
+    float mDampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY;
+    float mStiffness = SpringForce.STIFFNESS_MEDIUM;
+
+    public SpringProperty() {
+        this(0);
+    }
+
+    public SpringProperty(int flags) {
+        this.flags = flags;
+    }
+
+    public SpringProperty setDampingRatio(float dampingRatio) {
+        mDampingRatio = dampingRatio;
+        return this;
+    }
+
+    public SpringProperty setStiffness(float stiffness) {
+        mStiffness = stiffness;
+        return this;
+    }
+}
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
index 34d69e9..4e98781 100644
--- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
+++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@@ -42,11 +42,11 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.util.FlingBlockCheck;
-import com.android.launcher3.util.PendingAnimation;
 import com.android.launcher3.util.TouchController;
 
 /**
@@ -434,7 +434,7 @@
         maybeUpdateAtomicAnim(mFromState, targetState, targetState == mToState ? 1f : 0f);
         updateSwipeCompleteAnimation(anim, Math.max(duration, getRemainingAtomicDuration()),
                 targetState, velocity, fling);
-        mCurrentAnimation.dispatchOnStartWithVelocity(endProgress, progressVelocity);
+        mCurrentAnimation.dispatchOnStart();
         if (fling && targetState == LauncherState.ALL_APPS && !UNSTABLE_SPRINGS.get()) {
             mLauncher.getAppsView().addSpringFromFlingUpdateListener(anim, velocity);
         }
diff --git a/src/com/android/launcher3/util/PendingAnimation.java b/src/com/android/launcher3/util/PendingAnimation.java
deleted file mode 100644
index 617a38b..0000000
--- a/src/com/android/launcher3/util/PendingAnimation.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.util;
-
-import android.animation.AnimatorSet;
-import android.annotation.TargetApi;
-import android.os.Build;
-
-import java.util.ArrayList;
-import java.util.function.Consumer;
-
-/**
- * Utility class to keep track of a running animation.
- *
- * This class allows attaching end callbacks to an animation is intended to be used with
- * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case
- * AnimationListeners are not properly dispatched.
- */
-@TargetApi(Build.VERSION_CODES.O)
-public class PendingAnimation {
-
-    private final ArrayList<Consumer<OnEndListener>> mEndListeners = new ArrayList<>();
-
-    public final AnimatorSet anim;
-
-    public PendingAnimation(AnimatorSet anim) {
-        this.anim = anim;
-    }
-
-    public void finish(boolean isSuccess, int logAction) {
-        for (Consumer<OnEndListener> listeners : mEndListeners) {
-            listeners.accept(new OnEndListener(isSuccess, logAction));
-        }
-        mEndListeners.clear();
-    }
-
-    public void addEndListener(Consumer<OnEndListener> listener) {
-        mEndListeners.add(listener);
-    }
-
-    public static class OnEndListener {
-        public boolean isSuccess;
-        public int logAction;
-
-        public OnEndListener(boolean isSuccess, int logAction) {
-            this.isSuccess = isSuccess;
-            this.logAction = logAction;
-        }
-    }
-}