Moving some methods related to task launch animation to
recents view and related classes.
This allows the common animation to be used in fallback activity.

Bug: 75979063
Change-Id: I2b5bf5e66406621305b9a076793434f9c5cecdfd
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index f1471a7..8f2e104 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -17,7 +17,6 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
@@ -53,7 +52,6 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -93,8 +91,8 @@
     private static final int CLOSING_TRANSITION_DURATION_MS = 350;
 
     // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down.
-    private static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f;
-    private static final float ALL_APPS_PROGRESS_OVERSHOOT = 0.99581414f;
+    public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f;
+    public static final float ALL_APPS_PROGRESS_OVERSHOOT = 0.99581414f;
 
     private final DragLayer mDragLayer;
     private final Launcher mLauncher;
@@ -148,7 +146,14 @@
 
                     @Override
                     public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) {
-                        AnimatorSet anim = createLaunchAnimatorForView(v, targetCompats);
+                        Animator[] anims = composeRecentsLaunchAnimator(v, targetCompats);
+                        AnimatorSet anim = new AnimatorSet();
+                        if (anims != null) {
+                            anim.playTogether(anims);
+                        } else {
+                            anim.play(getLauncherAnimators(v, targetCompats));
+                            anim.play(getWindowAnimators(v, targetCompats));
+                        }
                         mLauncher.getStateManager().setCurrentAnimation(anim);
                         return anim;
                     }
@@ -167,19 +172,6 @@
         return getDefaultActivityLaunchOptions(launcher, v);
     }
 
-    private AnimatorSet createLaunchAnimatorForView(View v, RemoteAnimationTargetCompat[] targets) {
-        Animator[] anims = composeRecentsLaunchAnimator(v, new AnimConfig(targets,
-                RECENTS_LAUNCH_DURATION, Interpolators.TOUCH_RESPONSE_INTERPOLATOR));
-        AnimatorSet anim = new AnimatorSet();
-        if (anims != null) {
-            anim.playTogether(anims);
-        } else {
-            anim.play(getLauncherAnimators(v, targets));
-            anim.play(getWindowAnimators(v, targets));
-        }
-        return anim;
-    }
-
     /**
      * Try to find a TaskView that corresponds with the component of the launched view.
      *
@@ -238,32 +230,21 @@
         return taskView;
     }
 
-    public AnimatorSet composeUserControlledRecentsLaunchAnimator(TaskView v, AnimConfig config) {
-        Animator[] anims = composeRecentsLaunchAnimator(v, config);
-        AnimatorSet anim = new AnimatorSet();
-        anim.playTogether(anims);
-        return anim;
-    }
-
     /**
      * Composes the animations for a launch from the recents list if possible.
      */
-    private Animator[] composeRecentsLaunchAnimator(View v, AnimConfig config) {
+    private Animator[] composeRecentsLaunchAnimator(View v,
+            RemoteAnimationTargetCompat[] targets) {
         // Ensure recents is actually visible
         if (!mLauncher.getStateManager().getState().overviewUi) {
             return null;
         }
 
         RecentsView recentsView = mLauncher.getOverviewPanel();
-        boolean launcherClosing = launcherIsATargetWithMode(config.targets, MODE_CLOSING);
-        if (config.userControlled) {
-            // We don't pass any targets when creating a user-controlled animation. In this case,
-            // assume launcher is closing.
-            launcherClosing = true;
-        }
+        boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING);
         boolean skipLauncherChanges = !launcherClosing;
 
-        TaskView taskView = findTaskViewToLaunch(mLauncher, v, config.targets);
+        TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets);
         if (taskView == null) {
             return null;
         }
@@ -272,7 +253,10 @@
         Animator launcherAnim;
         final AnimatorListenerAdapter windowAnimEndListener;
         if (launcherClosing) {
-            launcherAnim = getRecentsLauncherAnimator(recentsView, taskView, config);
+            launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView);
+            launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+            launcherAnim.setDuration(RECENTS_LAUNCH_DURATION);
+
             // Make sure recents gets fixed up by resetting task alphas and scales, etc.
             windowAnimEndListener = mReapplyStateListener;
         } else {
@@ -289,150 +273,25 @@
             };
         }
 
-        Animator windowAnim = getRecentsWindowAnimator(taskView, skipLauncherChanges, config);
-        if (!config.userControlled) {
-            // Don't reset properties if the animation is user-controlled, as we will run the
-            // "real" (not user controlled) animation from where they left off when they let go.
-            windowAnim.addListener(windowAnimEndListener);
-        }
+        Animator windowAnim = getRecentsWindowAnimator(taskView, skipLauncherChanges, targets);
+        windowAnim.addListener(windowAnimEndListener);
         return new Animator[] {launcherAnim, windowAnim};
     }
 
     /**
-     * Animate adjacent tasks off screen while scaling up, and translate hotseat off screen as well.
-     *
-     * If launching one of the adjacent tasks, parallax the center task and other adjacent task
-     * to the right.
-     */
-    private Animator getRecentsLauncherAnimator(RecentsView recentsView, TaskView v,
-            AnimConfig config) {
-        AnimatorSet launcherAnimator = new AnimatorSet();
-
-        int launchedTaskIndex = recentsView.indexOfChild(v);
-        int centerTaskIndex = recentsView.getCurrentPage();
-        boolean launchingCenterTask = launchedTaskIndex == centerTaskIndex;
-        boolean isRtl = recentsView.isRtl();
-
-        RecentsAnimationInterpolator recentsInterpolator = getRecentsInterpolator(v);
-        TaskWindowBounds endInterpolation = recentsInterpolator.interpolate(1);
-        float toScale = endInterpolation.taskScale;
-        float toTranslationY = endInterpolation.taskY;
-        float displacementX = v.getWidth() * (toScale - v.getScaleX());
-        if (launchingCenterTask) {
-
-            if (launchedTaskIndex - 1 >= 0) {
-                TaskView adjacentPage1 = (TaskView) recentsView.getPageAt(launchedTaskIndex - 1);
-                ObjectAnimator adjacentTask1ScaleAndTranslate =
-                        LauncherAnimUtils.ofPropertyValuesHolder(adjacentPage1,
-                                new PropertyListBuilder()
-                                        .scale(adjacentPage1.getScaleX() * toScale)
-                                        .translationY(toTranslationY)
-                                        .translationX(isRtl ? displacementX : -displacementX)
-                                        .build());
-                adjacentTask1ScaleAndTranslate.setInterpolator(config.interpolator);
-                adjacentTask1ScaleAndTranslate.setDuration(config.duration);
-                launcherAnimator.play(adjacentTask1ScaleAndTranslate);
-            }
-            if (launchedTaskIndex + 1 < recentsView.getPageCount()) {
-                TaskView adjacentTask2 = (TaskView) recentsView.getPageAt(launchedTaskIndex + 1);
-                ObjectAnimator adjacentTask2ScaleAndTranslate =
-                        LauncherAnimUtils.ofPropertyValuesHolder(adjacentTask2,
-                                new PropertyListBuilder()
-                                        .scale(adjacentTask2.getScaleX() * toScale)
-                                        .translationY(toTranslationY)
-                                        .translationX(isRtl ? -displacementX : displacementX)
-                                        .build());
-                adjacentTask2ScaleAndTranslate.setInterpolator(config.interpolator);
-                adjacentTask2ScaleAndTranslate.setDuration(config.duration);
-                launcherAnimator.play(adjacentTask2ScaleAndTranslate);
-            }
-        } else {
-            // We are launching an adjacent task, so parallax the center and other adjacent task.
-            TaskView centerTask = (TaskView) recentsView.getPageAt(centerTaskIndex);
-            ObjectAnimator centerTaskParallaxOffscreen =
-                    LauncherAnimUtils.ofPropertyValuesHolder(centerTask,
-                            new PropertyListBuilder()
-                                    .translationX(isRtl ? -displacementX : displacementX)
-                                    .build());
-            centerTaskParallaxOffscreen.setInterpolator(config.interpolator);
-            centerTaskParallaxOffscreen.setDuration(config.duration);
-            launcherAnimator.play(centerTaskParallaxOffscreen);
-            int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - launchedTaskIndex);
-            if (otherAdjacentTaskIndex >= 0
-                    && otherAdjacentTaskIndex < recentsView.getPageCount()) {
-                TaskView otherAdjacentTask = (TaskView) recentsView.getPageAt(
-                        otherAdjacentTaskIndex);
-                ObjectAnimator otherAdjacentTaskParallaxOffscreen =
-                        LauncherAnimUtils.ofPropertyValuesHolder(otherAdjacentTask,
-                                new PropertyListBuilder()
-                                        .translationX(isRtl ? -displacementX : displacementX)
-                                        .scale(1)
-                                        .build());
-                otherAdjacentTaskParallaxOffscreen.setInterpolator(config.interpolator);
-                otherAdjacentTaskParallaxOffscreen.setDuration(config.duration);
-                launcherAnimator.play(otherAdjacentTaskParallaxOffscreen);
-            }
-        }
-
-        float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN;
-        LauncherState state = mLauncher.getStateManager().getState();
-        if ((state.getVisibleElements(mLauncher) & ALL_APPS_HEADER_EXTRA) != 0) {
-            float maxShiftRange = mDeviceProfile.heightPx;
-            float currShiftRange = mLauncher.getAllAppsController().getShiftRange();
-            allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange;
-        }
-        Animator allAppsSlideOut = ObjectAnimator.ofFloat(mLauncher.getAllAppsController(),
-                ALL_APPS_PROGRESS, allAppsProgressOffscreen);
-        allAppsSlideOut.setInterpolator(config.interpolator);
-        allAppsSlideOut.setDuration(config.duration);
-        launcherAnimator.play(allAppsSlideOut);
-
-        Workspace workspace = mLauncher.getWorkspace();
-        float[] workspaceScaleAndTranslation = NORMAL
-                .getWorkspaceScaleAndTranslation(mLauncher);
-        Animator recenterWorkspace = LauncherAnimUtils.ofPropertyValuesHolder(
-                workspace, new PropertyListBuilder()
-                        .translationX(workspaceScaleAndTranslation[1])
-                        .translationY(workspaceScaleAndTranslation[2])
-                        .build());
-        recenterWorkspace.setInterpolator(config.interpolator);
-        recenterWorkspace.setDuration(config.duration);
-        launcherAnimator.play(recenterWorkspace);
-        CellLayout currentWorkspacePage = (CellLayout) workspace.getPageAt(
-                workspace.getCurrentPage());
-
-        return launcherAnimator;
-    }
-
-    private RecentsAnimationInterpolator getRecentsInterpolator(TaskView v) {
-        Rect taskViewBounds = new Rect();
-        mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
-
-        // TODO: Use the actual target insets instead of the current thumbnail insets in case the
-        // device state has changed
-        return new RecentsAnimationInterpolator(
-                new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx),
-                v.getThumbnail().getInsets(),
-                taskViewBounds,
-                new Rect(0, v.getThumbnail().getTop(), 0, 0),
-                v.getScaleX(),
-                v.getTranslationX());
-    }
-
-    /**
      * @return Animator that controls the window of the opening targets for the recents launch
      * animation.
      */
     private ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipLauncherChanges,
-            AnimConfig config) {
-        final RecentsAnimationInterpolator recentsInterpolator = getRecentsInterpolator(v);
+            RemoteAnimationTargetCompat[] targets) {
+        final RecentsAnimationInterpolator recentsInterpolator = v.getRecentsInterpolator();
 
         Rect crop = new Rect();
         Matrix matrix = new Matrix();
 
         ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
-        appAnimator.setDuration(config.duration);
-        appAnimator.setInterpolator(config.interpolator);
+        appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+        appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
         appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             boolean isFirstFrame = true;
 
@@ -454,11 +313,9 @@
                     v.setScaleY(tw.taskScale);
                     v.setTranslationX(tw.taskX);
                     v.setTranslationY(tw.taskY);
-                    if (!config.userControlled) {
-                        // Defer fading out the view until after the app window gets faded in
-                        v.setAlpha(getValue(1f, 0f, alphaDuration, alphaDuration,
-                                appAnimator.getDuration() * percent, Interpolators.LINEAR));
-                    }
+                    // Defer fading out the view until after the app window gets faded in
+                    v.setAlpha(getValue(1f, 0f, alphaDuration, alphaDuration,
+                            appAnimator.getDuration() * percent, Interpolators.LINEAR));
                 }
 
                 matrix.setScale(tw.winScale, tw.winScale);
@@ -470,7 +327,7 @@
                         appAnimator.getDuration() * percent, Interpolators.LINEAR);
 
                 TransactionCompat t = new TransactionCompat();
-                for (RemoteAnimationTargetCompat target : config.targets) {
+                for (RemoteAnimationTargetCompat target : targets) {
                     if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
                         t.setAlpha(target.leash, alpha);
 
@@ -967,24 +824,4 @@
         newPercent = i.getInterpolation(newPercent);
         return end * newPercent + start * (1 - newPercent);
     }
-
-    public static class AnimConfig {
-        public RemoteAnimationTargetCompat[] targets;
-        public long duration;
-        public Interpolator interpolator;
-
-        public boolean userControlled = false;
-
-        public AnimConfig(RemoteAnimationTargetCompat[] targets, long duration,
-                Interpolator interpolator) {
-            this.targets = targets;
-            this.duration = duration;
-            this.interpolator = interpolator;
-        }
-
-        public AnimConfig(long duration, Interpolator interpolator) {
-            this(new RemoteAnimationTargetCompat[0], duration, interpolator);
-            userControlled = true;
-        }
-    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
index dca0018..369ae3a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java
@@ -20,7 +20,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
 import android.animation.ValueAnimator;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -28,8 +27,6 @@
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppTransitionManagerImpl;
-import com.android.launcher3.LauncherAppTransitionManagerImpl.AnimConfig;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
@@ -166,21 +163,20 @@
         if (goingUp) {
             mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
                     true /* animateTaskView */, true /* removeTask */, maxDuration);
-            mCurrentAnimation = AnimatorPlaybackController
-                    .wrap(mPendingAnimation.anim, maxDuration);
+
             mEndDisplacement = -mTaskBeingDragged.getHeight();
         } else {
-            LauncherAppTransitionManagerImpl appTransitionManager =
-                    (LauncherAppTransitionManagerImpl) mLauncher.getAppTransitionManager();
-            AnimatorSet anim = appTransitionManager.composeUserControlledRecentsLaunchAnimator(
-                    mTaskBeingDragged, new AnimConfig(maxDuration, Interpolators.ZOOM_IN));
-            mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+            mPendingAnimation = mRecentsView.createTaskLauncherAnimation(
+                    mTaskBeingDragged, maxDuration);
+            mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN);
 
             mTempCords[1] = mTaskBeingDragged.getHeight();
             dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
             mEndDisplacement = dl.getHeight() - mTempCords[1];
         }
 
+        mCurrentAnimation = AnimatorPlaybackController
+                .wrap(mPendingAnimation.anim, maxDuration);
         mCurrentAnimation.getTarget().addListener(this);
         mCurrentAnimation.dispatchOnStart();
         mProgressMultiplier = 1 / mEndDisplacement;
@@ -258,7 +254,6 @@
         }
         if (wasSuccess) {
             if (!mCurrentAnimationIsGoingUp) {
-                mTaskBeingDragged.launchTask(false);
                 mLauncher.getUserEventDispatcher().logTaskLaunch(logAction,
                         Direction.DOWN, mTaskBeingDragged.getTask().getTopComponent());
             }
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index ad0e253..d428f23 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -15,8 +15,14 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.launcher3.LauncherAppTransitionManagerImpl.ALL_APPS_PROGRESS_OFF_SCREEN;
+import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
 
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -30,6 +36,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 
 /**
@@ -137,4 +144,23 @@
         // Lazily update the empty message only when the task stack is reapplied
         updateEmptyMessage();
     }
+
+    /**
+     * Animates adjacent tasks and translate hotseat off screen as well.
+     */
+    @Override
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
+        AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv);
+
+        float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN;
+        LauncherState state = mActivity.getStateManager().getState();
+        if ((state.getVisibleElements(mActivity) & ALL_APPS_HEADER_EXTRA) != 0) {
+            float maxShiftRange = mActivity.getDeviceProfile().heightPx;
+            float currShiftRange = mActivity.getAllAppsController().getShiftRange();
+            allAppsProgressOffscreen = 1f + (maxShiftRange - currShiftRange) / maxShiftRange;
+        }
+        anim.play(ObjectAnimator.ofFloat(
+                mActivity.getAllAppsController(), ALL_APPS_PROGRESS, allAppsProgressOffscreen));
+        return anim;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7a5fd2f..bac8fc7 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -55,10 +55,13 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.PendingAnimation;
 import com.android.quickstep.QuickScrubController;
+import com.android.quickstep.RecentsAnimationInterpolator;
+import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
 import com.android.quickstep.RecentsModel;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.RecentsTaskLoader;
@@ -839,4 +842,94 @@
             canvas.restore();
         }
     }
+
+    /**
+     * Animate adjacent tasks off screen while scaling up.
+     *
+     * If launching one of the adjacent tasks, parallax the center task and other adjacent task
+     * to the right.
+     */
+    public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) {
+        AnimatorSet anim = new AnimatorSet();
+
+        int taskIndex = indexOfChild(tv);
+        int centerTaskIndex = getCurrentPage();
+        boolean launchingCenterTask = taskIndex == centerTaskIndex;
+
+        TaskWindowBounds endInterpolation = tv.getRecentsInterpolator().interpolate(1);
+        float toScale = endInterpolation.taskScale;
+        float toTranslationY = endInterpolation.taskY;
+
+        float displacementX = tv.getWidth() * (toScale - tv.getScaleX());
+        if (launchingCenterTask) {
+            if (taskIndex - 1 >= 0) {
+                anim.play(createAnimForChild(
+                        taskIndex - 1, toScale, displacementX, toTranslationY));
+            }
+            if (taskIndex + 1 < getPageCount()) {
+                anim.play(createAnimForChild(
+                        taskIndex + 1, toScale, -displacementX, toTranslationY));
+            }
+        } else {
+            // We are launching an adjacent task, so parallax the center and other adjacent task.
+            anim.play(ObjectAnimator.ofFloat(getPageAt(centerTaskIndex), TRANSLATION_X,
+                    mIsRtl ? -displacementX : displacementX));
+
+            int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - taskIndex);
+            if (otherAdjacentTaskIndex >= 0 && otherAdjacentTaskIndex < getPageCount()) {
+                anim.play(ObjectAnimator.ofPropertyValuesHolder(getPageAt(otherAdjacentTaskIndex),
+                        new PropertyListBuilder()
+                                .translationX(mIsRtl ? -displacementX : displacementX)
+                                .scale(1)
+                                .build()));
+            }
+        }
+        return anim;
+    }
+
+    private ObjectAnimator createAnimForChild(int childIndex, float toScale, float tx, float ty) {
+        View child = getChildAt(childIndex);
+        return ObjectAnimator.ofPropertyValuesHolder(child,
+                        new PropertyListBuilder()
+                                .scale(child.getScaleX() * toScale)
+                                .translationY(ty)
+                                .translationX(mIsRtl ? tx : -tx)
+                                .build());
+    }
+
+    public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) {
+        if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
+            throw new IllegalStateException("Another pending animation is still running");
+        }
+        AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv);
+
+        int count = getChildCount();
+        if (count == 0) {
+            return new PendingAnimation(anim);
+        }
+
+        final RecentsAnimationInterpolator recentsInterpolator = tv.getRecentsInterpolator();
+        ValueAnimator targetViewAnim = ValueAnimator.ofFloat(0, 1);
+        targetViewAnim.addUpdateListener((animation) -> {
+            float percent = animation.getAnimatedFraction();
+            TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
+            tv.setScaleX(tw.taskScale);
+            tv.setScaleY(tw.taskScale);
+            tv.setTranslationX(tw.taskX);
+            tv.setTranslationY(tw.taskY);
+        });
+        anim.play(targetViewAnim);
+        anim.setDuration(duration);
+
+        mPendingAnimation = new PendingAnimation(anim);
+        mPendingAnimation.addEndListener((isSuccess) -> {
+            if (isSuccess) {
+                tv.launchTask(false);
+            } else {
+                resetTaskVisuals();
+            }
+            mPendingAnimation = null;
+        });
+        return mPendingAnimation;
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 9884a48..a701862 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Outline;
+import android.graphics.Rect;
 import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.View;
@@ -29,7 +30,9 @@
 import android.widget.ImageView;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
+import com.android.quickstep.RecentsAnimationInterpolator;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
 import com.android.systemui.shared.recents.model.Task;
@@ -183,6 +186,23 @@
         return false;
     }
 
+    public RecentsAnimationInterpolator getRecentsInterpolator() {
+        Rect taskViewBounds = new Rect();
+        BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext());
+        DeviceProfile dp = activity.getDeviceProfile();
+        activity.getDragLayer().getDescendantRectRelativeToSelf(this, taskViewBounds);
+
+        // TODO: Use the actual target insets instead of the current thumbnail insets in case the
+        // device state has changed
+        return new RecentsAnimationInterpolator(
+                new Rect(0, 0, dp.widthPx, dp.heightPx),
+                getThumbnail().getInsets(),
+                taskViewBounds,
+                new Rect(0, getThumbnail().getTop(), 0, 0),
+                getScaleX(),
+                getTranslationX());
+    }
+
     private static final class TaskOutlineProvider extends ViewOutlineProvider {
 
         private final int mMarginTop;
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 6fea201..087752d 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -18,6 +18,7 @@
 import android.animation.Animator;
 import android.animation.Animator.AnimatorListener;
 import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 
 import java.util.ArrayList;
@@ -184,10 +185,14 @@
 
         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) {