Merge "Update shortcut icons according to new accessibility standards" into ub-launcher3-rvc-dev
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 0019ecb..6c64bf7 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -30,6 +30,7 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_VERTICAL_PROGRESS;
 import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
 import static com.android.quickstep.TaskViewUtils.getRecentsWindowAnimator;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -37,7 +38,6 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
-import android.util.FloatProperty;
 import android.view.View;
 
 import androidx.annotation.NonNull;
@@ -49,7 +49,6 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationBuilder;
 import com.android.launcher3.states.StateAnimationConfig;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.quickstep.util.AppWindowAnimationHelper;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -199,11 +198,10 @@
                 return ObjectAnimator.ofFloat(mLauncher.getOverviewPanel(),
                         RecentsView.CONTENT_ALPHA, values);
             case INDEX_RECENTS_TRANSLATE_X_ANIM:
-                PagedOrientationHandler orientationHandler =
-                    ((RecentsView)mLauncher.getOverviewPanel()).getPagedViewOrientedState()
-                        .getOrientationHandler();
-                FloatProperty<View> translate = orientationHandler.getPrimaryViewTranslate();
-                return new SpringAnimationBuilder<>(mLauncher.getOverviewPanel(), translate)
+                // TODO: Do not assume motion across X axis for adjacent page
+                return new SpringAnimationBuilder<>(
+                        mLauncher.getOverviewPanel(), ADJACENT_PAGE_OFFSET)
+                        .setMinimumVisibleChange(1f / mLauncher.getOverviewPanel().getWidth())
                         .setDampingRatio(0.8f)
                         .setStiffness(250)
                         .setValues(values)
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
index a87d6d1..e57e841 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/BackgroundAppState.java
@@ -61,13 +61,16 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {getOverviewScale(launcher), NO_OFFSET};
+    }
+
+    private float getOverviewScale(Launcher launcher) {
         // Initialize the recents view scale to what it would be when starting swipe up
         RecentsView recentsView = launcher.getOverviewPanel();
         int taskCount = recentsView.getTaskViewCount();
-        if (taskCount == 0) {
-            return super.getOverviewScaleAndTranslation(launcher);
-        }
+        if (taskCount == 0) return 1;
+
         TaskView dummyTask;
         if (recentsView.getCurrentPage() >= recentsView.getTaskViewStartIndex()) {
             if (recentsView.getCurrentPage() <= taskCount - 1) {
@@ -78,8 +81,8 @@
         } else {
             dummyTask = recentsView.getTaskViewAt(0);
         }
-        return recentsView.getTempAppWindowAnimationHelper().updateForFullscreenOverview(dummyTask)
-                .getScaleAndTranslation();
+        return recentsView.getTempAppWindowAnimationHelper()
+                .updateForFullscreenOverview(dummyTask).getSrcToTargetScale();
     }
 
     @Override
@@ -106,7 +109,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
index 1288e7b..b27f16a 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewPeekState.java
@@ -24,24 +24,18 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.states.StateAnimationConfig;
 
 public class OverviewPeekState extends OverviewState {
+    private static final float OVERVIEW_OFFSET = 0.7f;
+
     public OverviewPeekState(int id) {
         super(id);
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        ScaleAndTranslation result = super.getOverviewScaleAndTranslation(launcher);
-        result.translationX = NORMAL.getOverviewScaleAndTranslation(launcher).translationX
-                - launcher.getResources().getDimension(R.dimen.overview_peek_distance);
-        if (Utilities.isRtl(launcher.getResources())) {
-            result.translationX = -result.translationX;
-        }
-        return result;
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, OVERVIEW_OFFSET};
     }
 
     @Override
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
index fc28da3..e44f59f 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -24,7 +24,6 @@
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_7;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
 import static com.android.launcher3.logging.LoggerUtils.newContainerTarget;
-import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
@@ -123,8 +122,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1f, 0f, 0f);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {NO_SCALE, NO_OFFSET};
     }
 
     @Override
@@ -204,7 +203,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
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 c92a872..f4f8bc9 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
@@ -40,17 +40,15 @@
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.CANCEL;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.HIDE;
 import static com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState.PEEK;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.graphics.PointF;
 import android.view.MotionEvent;
-import android.view.View;
 import android.view.animation.Interpolator;
 
 import com.android.launcher3.BaseQuickstepLauncher;
@@ -59,6 +57,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
@@ -237,58 +236,32 @@
     private void setupOverviewAnimators() {
         final LauncherState fromState = QUICK_SWITCH;
         final LauncherState toState = OVERVIEW;
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation = fromState
-                .getOverviewScaleAndTranslation(mLauncher);
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = toState
-                .getOverviewScaleAndTranslation(mLauncher);
-        // Update RecentView's translationX to have it start offscreen.
-        float startScale = Utilities.mapRange(
-                SCALE_DOWN_INTERPOLATOR.getInterpolation(Y_ANIM_MIN_PROGRESS),
-                fromScaleAndTranslation.scale,
-                toScaleAndTranslation.scale);
-        fromScaleAndTranslation.translationX = mRecentsView.getOffscreenTranslationX(startScale);
 
         // Set RecentView's initial properties.
-        mRecentsView.setScaleX(fromScaleAndTranslation.scale);
-        mRecentsView.setScaleY(fromScaleAndTranslation.scale);
-        mRecentsView.setTranslationX(fromScaleAndTranslation.translationX);
-        mRecentsView.setTranslationY(fromScaleAndTranslation.translationY);
+        SCALE_PROPERTY.set(mRecentsView, fromState.getOverviewScaleAndOffset(mLauncher)[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, 1f);
         mRecentsView.setContentAlpha(1);
         mRecentsView.setFullscreenProgress(fromState.getOverviewFullscreenProgress());
 
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
         // As we drag right, animate the following properties:
         //   - RecentsView translationX
         //   - OverviewScrim
-        AnimatorSet xOverviewAnim = new AnimatorSet();
-        xOverviewAnim.play(ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_X,
-                toScaleAndTranslation.translationX));
-        xOverviewAnim.play(ObjectAnimator.ofFloat(
-                mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
-                toState.getOverviewScrimAlpha(mLauncher)));
-        long xAccuracy = (long) (mXRange * 2);
-        xOverviewAnim.setDuration(xAccuracy);
-        mXOverviewAnim = AnimatorPlaybackController.wrap(xOverviewAnim, xAccuracy);
+        PendingAnimation xAnim = new PendingAnimation((long) (mXRange * 2));
+        xAnim.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1], LINEAR);
+        xAnim.setFloat(mLauncher.getDragLayer().getOverviewScrim(), OverviewScrim.SCRIM_PROGRESS,
+                toState.getOverviewScrimAlpha(mLauncher), LINEAR);
+        mXOverviewAnim = xAnim.createPlaybackController();
         mXOverviewAnim.dispatchOnStart();
 
         // As we drag up, animate the following properties:
-        //   - RecentsView translationY
         //   - RecentsView scale
         //   - RecentsView fullscreenProgress
-        AnimatorSet yAnimation = new AnimatorSet();
-        Animator translateYAnim = ObjectAnimator.ofFloat(mRecentsView, View.TRANSLATION_Y,
-                toScaleAndTranslation.translationY);
-        Animator scaleAnim = ObjectAnimator.ofFloat(mRecentsView, SCALE_PROPERTY,
-                toScaleAndTranslation.scale);
-        Animator fullscreenProgressAnim = ObjectAnimator.ofFloat(mRecentsView, FULLSCREEN_PROGRESS,
-                fromState.getOverviewFullscreenProgress(), toState.getOverviewFullscreenProgress());
-        scaleAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        fullscreenProgressAnim.setInterpolator(SCALE_DOWN_INTERPOLATOR);
-        yAnimation.play(translateYAnim);
-        yAnimation.play(scaleAnim);
-        yAnimation.play(fullscreenProgressAnim);
-        long yAccuracy = (long) (mYRange * 2);
-        yAnimation.setDuration(yAccuracy);
-        mYOverviewAnim = AnimatorPlaybackController.wrap(yAnimation, yAccuracy);
+        PendingAnimation yAnim = new PendingAnimation((long) (mYRange * 2));
+        yAnim.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0], SCALE_DOWN_INTERPOLATOR);
+        yAnim.setFloat(mRecentsView, FULLSCREEN_PROGRESS,
+                toState.getOverviewFullscreenProgress(), SCALE_DOWN_INTERPOLATOR);
+        mYOverviewAnim = yAnim.createPlaybackController();
         mYOverviewAnim.dispatchOnStart();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
index 3513ad5..32ab98b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/BaseSwipeUpHandler.java
@@ -135,8 +135,15 @@
         mInputConsumer = inputConsumer;
         mAppWindowAnimationHelper = new AppWindowAnimationHelper(context);
         mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+    }
+
+    /**
+     * To be called at the end of constructor of subclasses. This calls various methods which can
+     * depend on proper class initialization.
+     */
+    protected void initAfterSubclassConstructor() {
         initTransitionEndpoints(InvariantDeviceProfile.INSTANCE.get(mContext)
-            .getDeviceProfile(mContext));
+                .getDeviceProfile(mContext));
     }
 
     protected void performHapticFeedback() {
@@ -241,6 +248,10 @@
         return mRecentsAnimationTargets != null && mRecentsAnimationTargets.hasTargets();
     }
 
+    protected void updateSource(Rect stackBounds, RemoteAnimationTargetCompat runningTarget) {
+        mAppWindowAnimationHelper.updateSource(stackBounds, runningTarget);
+    }
+
     @Override
     public void onRecentsAnimationStart(RecentsAnimationController recentsAnimationController,
             RecentsAnimationTargets targets) {
@@ -264,7 +275,7 @@
         dp.updateInsets(targets.homeContentInsets);
         dp.updateIsSeascape(mContext);
         if (runningTaskTarget != null) {
-            mAppWindowAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget);
+            updateSource(overviewStackBounds, runningTaskTarget);
         }
 
         mAppWindowAnimationHelper.prepareAnimation(dp, false /* isOpening */);
@@ -314,6 +325,7 @@
 
         mTransitionDragLength = mActivityInterface.getSwipeUpDestinationAndLength(
                 dp, mContext, TEMP_RECT);
+
         if (!dp.isMultiWindowMode) {
             // When updating the target rect, also update the home bounds since the location on
             // screen of the launcher window may be stale (position is not updated until first
@@ -409,11 +421,12 @@
      */
     protected void applyTransformUnchecked() {
         float shift = mCurrentShift.value;
-        float offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffset();
+        float offset = mRecentsView == null ? 0 : mRecentsView.getScrollOffsetScaled();
         float taskSize = getOrientationHandler()
             .getPrimarySize(mAppWindowAnimationHelper.getTargetRect());
         float offsetScale = getTaskCurveScaleForOffset(offset, taskSize);
-        mTransformParams.setProgress(shift)
+        mTransformParams
+                .setProgress(shift)
                 .setOffset(offset)
                 .setOffsetScale(offsetScale)
                 .setTargetSet(mRecentsAnimationTargets)
@@ -459,6 +472,7 @@
             FloatingIconView fiv = (FloatingIconView) floatingView;
             anim.addAnimatorListener(fiv);
             fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
+            fiv.setFastFinishRunnable(anim::end);
         }
 
         AnimatorPlaybackController homeAnim = homeAnimationFactory.createActivityAnimationToHome();
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
index b4492d8..1b2979b 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/FallbackSwipeHandler.java
@@ -129,6 +129,7 @@
         mEndTargetAnimationParams.put(LAST_TASK, new EndTargetAnimationParams(0, 150, 1));
         mEndTargetAnimationParams.put(NEW_TASK, new EndTargetAnimationParams(0, 150, 1));
 
+        initAfterSubclassConstructor();
         initStateCallbacks();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
index 455ae76..b4764dc 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherActivityInterface.java
@@ -15,8 +15,6 @@
  */
 package com.android.quickstep;
 
-import static android.view.View.TRANSLATION_Y;
-
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_FADE_ANIM;
 import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_TRANSLATE_X_ANIM;
@@ -25,15 +23,14 @@
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.INSTANT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.quickstep.LauncherSwipeHandler.RECENTS_ATTACH_DURATION;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -57,7 +54,6 @@
 import com.android.launcher3.appprediction.PredictionUiStateManager;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statehandlers.DepthController.ClampedDepthProperty;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.views.FloatingIconView;
 import com.android.quickstep.SysUINavigationMode.Mode;
@@ -81,7 +77,6 @@
  */
 public final class LauncherActivityInterface implements BaseActivityInterface<Launcher> {
 
-    private Runnable mAdjustInterpolatorsRunnable;
     private Pair<Float, Float> mSwipeUpPullbackStartAndMaxProgress =
             BaseActivityInterface.super.getSwipeUpPullbackStartAndMaxProgress();
 
@@ -243,14 +238,6 @@
             }
 
             @Override
-            public void adjustActivityControllerInterpolators() {
-                if (mAdjustInterpolatorsRunnable != null) {
-                    mAdjustInterpolatorsRunnable.run();
-                    mAdjustInterpolatorsRunnable = null;
-                }
-            }
-
-            @Override
             public void onTransitionCancelled() {
                 launcher.getStateManager().goToState(startState, false /* animate */);
             }
@@ -272,42 +259,24 @@
                         .createStateElementAnimation(
                         INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
 
-                int runningTaskIndex = recentsView.getRunningTaskIndex();
-                if (runningTaskIndex == recentsView.getTaskViewStartIndex()) {
-                    // If we are on the first task (we haven't quick switched), translate recents in
-                    // from the side. Calculate the start translation based on current scale/scroll.
-                    float currScale = recentsView.getScaleX();
-                    float scrollOffsetX = recentsView.getScrollOffset();
-                    float offscreenX = recentsView.getOffscreenTranslationX(currScale);
-
-                    float fromTranslation = attached ? offscreenX - scrollOffsetX : 0;
-                    float toTranslation = attached ? 0 : offscreenX - scrollOffsetX;
-                    launcher.getStateManager()
-                            .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
-
-                    PagedOrientationHandler pagedOrientationHandler =
-                        recentsView.getPagedViewOrientedState().getOrientationHandler();
-                    if (!recentsView.isShown() && animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, fromTranslation);
-                    } else {
-                        fromTranslation =
-                            pagedOrientationHandler.getPrimaryViewTranslate().get(recentsView);
-                    }
-
-                    if (!animate) {
-                        pagedOrientationHandler
-                            .getPrimaryViewTranslate().set(recentsView, toTranslation);
-                    } else {
-                        launcher.getStateManager().createStateElementAnimation(
-                                INDEX_RECENTS_TRANSLATE_X_ANIM,
-                                fromTranslation, toTranslation).start();
-                    }
-
-                    fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
+                float fromTranslation = attached ? 1 : 0;
+                float toTranslation = attached ? 0 : 1;
+                launcher.getStateManager()
+                        .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
+                if (!recentsView.isShown() && animate) {
+                    ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
                 } else {
-                    fadeAnim.setInterpolator(ACCEL_DEACCEL);
+                    fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
                 }
+                if (!animate) {
+                    ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
+                } else {
+                    launcher.getStateManager().createStateElementAnimation(
+                            INDEX_RECENTS_TRANSLATE_X_ANIM,
+                            fromTranslation, toTranslation).start();
+                }
+
+                fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
                 fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
             }
         };
@@ -365,51 +334,20 @@
             return;
         }
 
-        LauncherState.ScaleAndTranslation fromScaleAndTranslation
-                = fromState.getOverviewScaleAndTranslation(launcher);
-        LauncherState.ScaleAndTranslation endScaleAndTranslation
-                = endState.getOverviewScaleAndTranslation(launcher);
-        float fromTranslationY = fromScaleAndTranslation.translationY;
-        float endTranslationY = endScaleAndTranslation.translationY;
         float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
         float endFullscreenProgress = endState.getOverviewFullscreenProgress();
 
-        Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
-                fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
-        Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
-                fromTranslationY, endTranslationY);
+        float fromScale = fromState.getOverviewScaleAndOffset(launcher)[0];
+        float endScale = endState.getOverviewScaleAndOffset(launcher)[0];
+
+        Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, fromScale, endScale);
         Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
                 RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
-        anim.playTogether(scale, translateY, applyFullscreenProgress);
-
-        mAdjustInterpolatorsRunnable = () -> {
-            // Adjust the translateY interpolator to account for the running task's top inset.
-            // When progress <= 1, this is handled by each task view as they set their fullscreen
-            // progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
-            // recents as a whole needs to translate further to keep up with the app window.
-            TaskView runningTaskView = recentsView.getRunningTaskView();
-            if (runningTaskView == null) {
-                runningTaskView = recentsView.getCurrentPageTaskView();
-                if (runningTaskView == null) {
-                    // There are no task views in LockTask mode when Overview is enabled.
-                    return;
-                }
-            }
-            TimeInterpolator oldInterpolator = translateY.getInterpolator();
-            Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
-            float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
-            float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
-            translateY.setInterpolator(t -> {
-                float newT = oldInterpolator.getInterpolation(t);
-                return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
-            });
-        };
+        anim.playTogether(scale, applyFullscreenProgress);
 
         // Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
-        float pullbackStartProgress = (0.75f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
-        float pullbackMaxProgress = (0.5f - fromScaleAndTranslation.scale)
-                / (endScaleAndTranslation.scale - fromScaleAndTranslation.scale);
+        float pullbackStartProgress = (0.75f - fromScale) / (endScale - fromScale);
+        float pullbackMaxProgress = (0.5f - fromScale) / (endScale - fromScale);
         mSwipeUpPullbackStartAndMaxProgress = new Pair<>(
                 pullbackStartProgress, pullbackMaxProgress);
     }
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 31a2814..5a64382 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/LauncherSwipeHandler.java
@@ -17,6 +17,8 @@
 
 import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER;
 import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
+import static com.android.launcher3.LauncherState.BACKGROUND_APP;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.DEACCEL;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.OVERSHOOT_1_2;
@@ -39,16 +41,17 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
 import android.os.SystemClock;
-import android.util.Log;
 import android.view.View;
 import android.view.View.OnApplyWindowInsetsListener;
 import android.view.ViewTreeObserver.OnDrawListener;
@@ -67,7 +70,6 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.testing.TestProtocol;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -78,9 +80,11 @@
 import com.android.quickstep.inputconsumers.OverviewInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
+import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RectFSpringAnim;
 import com.android.quickstep.util.ShelfPeekAnim;
 import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
+import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.views.LiveTileOverlay;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
@@ -177,6 +181,9 @@
     private AnimatorPlaybackController mLauncherTransitionController;
     private boolean mHasLauncherTransitionControllerStarted;
 
+    private final TaskViewSimulator mTaskViewSimulator;
+    private AnimatorPlaybackController mWindowTransitionController;
+
     private AnimationFactory mAnimationFactory = (t) -> { };
 
     private boolean mWasLauncherAlreadyVisible;
@@ -201,6 +208,9 @@
         mTaskAnimationManager = taskAnimationManager;
         mTouchTimeMs = touchTimeMs;
         mContinuingLastGesture = continuingLastGesture;
+        mTaskViewSimulator = new TaskViewSimulator(context, LayoutUtils::calculateLauncherTaskSize);
+
+        initAfterSubclassConstructor();
         initStateCallbacks();
     }
 
@@ -473,23 +483,11 @@
         } else if (mContinuingLastGesture
                 && mRecentsView.getRunningTaskIndex() != mRecentsView.getNextPage()) {
             recentsAttachedToAppWindow = true;
-            animate = false;
         } else if (runningTaskTarget != null && isNotInRecents(runningTaskTarget)) {
             // The window is going away so make sure recents is always visible in this case.
             recentsAttachedToAppWindow = true;
-            animate = false;
         } else {
             recentsAttachedToAppWindow = mIsShelfPeeking || mIsLikelyToStartNewTask;
-            if (animate) {
-                // Only animate if an adjacent task view is visible on screen.
-                TaskView adjacentTask1 = mRecentsView.getNextTaskView();
-                TaskView adjacentTask2 = mRecentsView.getPreviousTaskView();
-                float prevTranslationX = mRecentsView.getTranslationX();
-                mRecentsView.setTranslationX(0);
-                animate = (adjacentTask1 != null && adjacentTask1.getGlobalVisibleRect(TEMP_RECT))
-                        || (adjacentTask2 != null && adjacentTask2.getGlobalVisibleRect(TEMP_RECT));
-                mRecentsView.setTranslationX(prevTranslationX);
-            }
         }
         mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
     }
@@ -523,6 +521,34 @@
         mAnimationFactory.createActivityInterface(mTransitionDragLength);
     }
 
+    @Override
+    protected void updateSource(Rect stackBounds, RemoteAnimationTargetCompat runningTarget) {
+        super.updateSource(stackBounds, runningTarget);
+        mTaskViewSimulator.setPreview(runningTarget, mRecentsAnimationTargets);
+    }
+
+    @Override
+    protected void initTransitionEndpoints(DeviceProfile dp) {
+        super.initTransitionEndpoints(dp);
+        mTaskViewSimulator.setDp(dp, false /* isOpening */);
+        mTaskViewSimulator.setLayoutRotation(
+                mDeviceState.getCurrentActiveRotation(),
+                mDeviceState.getDisplayRotation());
+
+        AnimatorSet anim = new AnimatorSet();
+        anim.setDuration(mTransitionDragLength * 2);
+        anim.setInterpolator(t -> t * mDragLengthFactor);
+        anim.play(ObjectAnimator.ofFloat(mTaskViewSimulator.recentsViewScale,
+                AnimatedFloat.VALUE,
+                mTaskViewSimulator.getFullScreenScale(), 1));
+        anim.play(ObjectAnimator.ofFloat(mTaskViewSimulator.fullScreenProgress,
+                AnimatedFloat.VALUE,
+                BACKGROUND_APP.getOverviewFullscreenProgress(),
+                OVERVIEW.getOverviewFullscreenProgress()));
+        mWindowTransitionController =
+                AnimatorPlaybackController.wrap(anim, mTransitionDragLength * 2);
+    }
+
     /**
      * We don't want to change mLauncherTransitionController if mGestureState.getEndTarget() == HOME
      * (it has its own animation) or if we're already animating the current controller.
@@ -542,7 +568,6 @@
     private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) {
         mLauncherTransitionController = anim;
         mLauncherTransitionController.dispatchSetInterpolator(t -> t * mDragLengthFactor);
-        mAnimationFactory.adjustActivityControllerInterpolators();
         mLauncherTransitionController.dispatchOnStart();
         updateLauncherTransitionProgress();
     }
@@ -555,7 +580,9 @@
     @Override
     public void updateFinalShift() {
         if (mRecentsAnimationTargets != null) {
-            applyTransformUnchecked();
+            // Base class expects applyTransformUnchecked to be called here.
+            // TODO: Remove this dependency for swipe-up animation.
+            // applyTransformUnchecked();
             updateSysUiFlags(mCurrentShift.value);
         }
 
@@ -575,6 +602,16 @@
             }
         }
 
+        if (mWindowTransitionController != null) {
+            float progress = mCurrentShift.value / mDragLengthFactor;
+            mWindowTransitionController.setPlayFraction(progress);
+            mTransformParams
+                    .setTargetSet(mRecentsAnimationTargets)
+                    .setLauncherOnTop(true);
+
+            mTaskViewSimulator.setScroll(mRecentsView == null ? 0 : mRecentsView.getScrollOffset());
+            mTaskViewSimulator.apply(mTransformParams);
+        }
         updateLauncherTransitionProgress();
     }
 
@@ -1020,7 +1057,6 @@
             mLauncherTransitionController.dispatchSetInterpolator(t -> end);
         } else {
             mLauncherTransitionController.dispatchSetInterpolator(adjustedInterpolator);
-            mAnimationFactory.adjustActivityControllerInterpolators();
         }
         mLauncherTransitionController.getAnimationPlayer().setDuration(Math.max(0, duration));
 
@@ -1296,6 +1332,7 @@
 
     private void setTargetAlphaProvider(TargetAlphaProvider provider) {
         mAppWindowAnimationHelper.setTaskAlphaCallback(provider);
+        mTaskViewSimulator.setTaskAlphaCallback(provider);
         updateFinalShift();
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 6d1bf11..235ac16 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -26,7 +26,6 @@
 import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.Utilities;
 import com.android.quickstep.RecentsActivity;
 import com.android.quickstep.util.LayoutUtils;
@@ -58,7 +57,6 @@
     private boolean mInOverviewState = true;
 
     private float mZoomScale = 1f;
-    private float mZoomTranslationY = 0f;
 
     private RunningTaskInfo mRunningTaskInfo;
 
@@ -145,14 +143,11 @@
 
         if (getTaskViewCount() == 0) {
             mZoomScale = 1f;
-            mZoomTranslationY = 0f;
         } else {
             TaskView dummyTask = getTaskViewAt(0);
-            ScaleAndTranslation sat = getTempAppWindowAnimationHelper()
+            mZoomScale = getTempAppWindowAnimationHelper()
                     .updateForFullscreenOverview(dummyTask)
-                    .getScaleAndTranslation();
-            mZoomScale = sat.scale;
-            mZoomTranslationY = sat.translationY;
+                    .getSrcToTargetScale();
         }
 
         setZoomProgress(mZoomInProgress);
@@ -161,7 +156,6 @@
     public void setZoomProgress(float progress) {
         mZoomInProgress = progress;
         SCALE_PROPERTY.set(this, Utilities.mapRange(mZoomInProgress, 1, mZoomScale));
-        TRANSLATION_Y.set(this, Utilities.mapRange(mZoomInProgress, 0, mZoomTranslationY));
         FULLSCREEN_PROGRESS.set(this, mZoomInProgress);
     }
 
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
index b739838..00329b8 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/AppWindowAnimationHelper.java
@@ -30,13 +30,11 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Build;
-import android.view.Surface;
 
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.Interpolators;
@@ -60,7 +58,7 @@
 public class AppWindowAnimationHelper {
 
     // The bounds of the source app in device coordinates
-    private final Rect mSourceStackBounds = new Rect();
+    private final RectF mSourceStackBounds = new RectF();
     // The insets of the source app
     private final Rect mSourceInsets = new Rect();
     // The source app bounds with the source insets applied, in the device coordinates
@@ -159,14 +157,10 @@
         mSourceRect.set(scaledTargetRect);
     }
 
-    private float getSrcToTargetScale() {
-        if (mOrientedState == null
-                || mOrientedState.isHomeRotationAllowed()
-                || mOrientedState.isDisplayPhoneNatural()) {
-            return mSourceRect.width() / mTargetRect.width();
-        } else {
-            return mSourceRect.height() / mTargetRect.height();
-        }
+    public float getSrcToTargetScale() {
+        return LayoutUtils.getTaskScale(mOrientedState,
+                mSourceRect.width(), mSourceRect.height(),
+                mTargetRect.width(), mTargetRect.height());
     }
 
     public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
@@ -378,15 +372,6 @@
         return this;
     }
 
-    /**
-     * @return The source rect's scale and translation relative to the target rect.
-     */
-    public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
-        float scale = getSrcToTargetScale();
-        float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
-        return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
-    }
-
     private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
         SystemUiProxy proxy = SystemUiProxy.INSTANCE.get(activity);
         if (proxy.isActive()) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
index dde7605..8a6c4a1 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/RectFSpringAnim.java
@@ -207,9 +207,23 @@
                 mRectScaleAnim.skipToEnd();
             }
         }
+        mRectXAnimEnded = true;
+        mRectYAnimEnded = true;
+        mRectScaleAnimEnded = true;
+        maybeOnEnd();
+    }
+
+    private boolean isEnded() {
+        return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
     }
 
     private void onUpdate() {
+        if (isEnded()) {
+            // Prevent further updates from being called. This can happen between callbacks for
+            // ending the x/y/scale animations.
+            return;
+        }
+
         if (!mOnUpdateListeners.isEmpty()) {
             float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
                     mTargetRect.width());
@@ -229,7 +243,7 @@
     }
 
     private void maybeOnEnd() {
-        if (mAnimsStarted && mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded) {
+        if (mAnimsStarted && isEnded()) {
             mAnimsStarted = false;
             for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
                 animatorListener.onAnimationEnd(null);
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
new file mode 100644
index 0000000..0131fdf
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -0,0 +1,284 @@
+/*
+ * 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.quickstep.util;
+
+import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
+import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
+import static com.android.quickstep.util.AppWindowAnimationHelper.applySurfaceParams;
+import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
+import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.touch.PagedOrientationHandler;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.RecentsAnimationTargets;
+import com.android.quickstep.util.AppWindowAnimationHelper.TargetAlphaProvider;
+import com.android.quickstep.util.AppWindowAnimationHelper.TransformParams;
+import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
+import com.android.quickstep.views.TaskView;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
+
+/**
+ * A utility class which emulates the layout behavior of TaskView and RecentsView
+ */
+public class TaskViewSimulator {
+
+    private final Rect mTmpCropRect = new Rect();
+    private final RectF mTempRectF = new RectF();
+
+    private final RecentsOrientedState mOrientationState;
+    private final Context mContext;
+    private final TaskSizeProvider mSizeProvider;
+
+    private final Rect mTaskRect = new Rect();
+    private final PointF mPivot = new PointF();
+    private DeviceProfile mDp;
+
+    private final Matrix mMatrix = new Matrix();
+    private RemoteAnimationTargetCompat mRunningTarget;
+    private RecentsAnimationTargets mAllTargets;
+
+    // Whether to boost the opening animation target layers, or the closing
+    private int mBoostModeTargetLayers = -1;
+    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
+
+    // Thumbnail view properties
+    private final Rect mThumbnailPosition = new Rect();
+    private final ThumbnailData mThumbnailData = new ThumbnailData();
+    private final PreviewPositionHelper mPositionHelper;
+    private final Matrix mInversePositionMatrix = new Matrix();
+
+    // TaskView properties
+    private final FullscreenDrawParams mCurrentFullscreenParams;
+    private float mCurveScale = 1;
+
+    // RecentsView properties
+    public final AnimatedFloat recentsViewScale = new AnimatedFloat(() -> { });
+    public final AnimatedFloat fullScreenProgress = new AnimatedFloat(() -> { });
+    private final ScrollState mScrollState = new ScrollState();
+    private final int mPageSpacing;
+
+    // Cached calculations
+    private boolean mLayoutValid = false;
+    private boolean mScrollValid = false;
+
+    public TaskViewSimulator(Context context, TaskSizeProvider sizeProvider) {
+        mContext = context;
+        mSizeProvider = sizeProvider;
+        mPositionHelper = new PreviewPositionHelper(context);
+        mOrientationState = new RecentsOrientedState(context);
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
+        mPageSpacing = context.getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+    }
+
+    /**
+     * Sets the device profile for the current state
+     */
+    public void setDp(DeviceProfile dp, boolean isOpening) {
+        mDp = dp;
+        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int)
+     */
+    public void setLayoutRotation(int touchRotation, int displayRotation) {
+        if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
+            return;
+        }
+        mOrientationState.update(touchRotation, displayRotation,
+                mOrientationState.getLauncherRotation());
+        mLayoutValid = false;
+    }
+
+    /**
+     * @see com.android.quickstep.views.RecentsView#FULLSCREEN_PROGRESS
+     */
+    public float getFullScreenScale() {
+        if (mDp == null) {
+            return 1;
+        }
+        mSizeProvider.calculateTaskSize(mContext, mDp, mTaskRect);
+        return mOrientationState.getFullScreenScaleAndPivot(mTaskRect, mDp, mPivot);
+    }
+
+    /**
+     * Sets the targets which the simulator will control
+     */
+    public void setPreview(
+            RemoteAnimationTargetCompat runningTarget, RecentsAnimationTargets allTargets) {
+        mRunningTarget = runningTarget;
+        mAllTargets = allTargets;
+
+        mThumbnailData.insets.set(mRunningTarget.contentInsets);
+        // TODO: What is this?
+        mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
+
+        mThumbnailPosition.set(runningTarget.screenSpaceBounds);
+        // TODO: Should sourceContainerBounds already have this offset?
+        mThumbnailPosition.offsetTo(mRunningTarget.position.x, mRunningTarget.position.y);
+
+        mLayoutValid = false;
+    }
+
+    /**
+     * Updates the scroll for RecentsView
+     */
+    public void setScroll(int scroll) {
+        if (mScrollState.scroll != scroll) {
+            mScrollState.scroll = scroll;
+            mScrollValid = false;
+        }
+    }
+
+    /**
+     * Sets an alternate function which can be used to control the alpha
+     */
+    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
+        mTaskAlphaCallback = callback;
+    }
+
+    /**
+     * Applies the target to the previously set parameters
+     */
+    public void apply(TransformParams params) {
+        if (mDp == null || mRunningTarget == null) {
+            return;
+        }
+        if (!mLayoutValid) {
+            mLayoutValid = true;
+
+            getFullScreenScale();
+            mThumbnailData.rotation = FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()
+                    ? mOrientationState.getDisplayRotation() : mPositionHelper.getCurrentRotation();
+
+            mPositionHelper.updateThumbnailMatrix(mThumbnailPosition, mThumbnailData,
+                    mDp.isMultiWindowMode, mTaskRect.width(), mTaskRect.height());
+
+            mPositionHelper.getMatrix().invert(mInversePositionMatrix);
+
+            PagedOrientationHandler poh = mOrientationState.getOrientationHandler();
+            mScrollState.halfPageSize =
+                    poh.getPrimaryValue(mTaskRect.width(), mTaskRect.height()) / 2;
+            mScrollState.halfScreenSize = poh.getPrimaryValue(mDp.widthPx, mDp.heightPx) / 2;
+            mScrollValid = false;
+        }
+
+        if (!mScrollValid) {
+            mScrollValid = true;
+            int start = mOrientationState.getOrientationHandler()
+                    .getPrimaryValue(mTaskRect.left, mTaskRect.top);
+            mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
+            mScrollState.updateInterpolation(start, mPageSpacing);
+            mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
+        }
+
+        float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
+        mCurrentFullscreenParams.setProgress(
+                progress, recentsViewScale.value, mTaskRect.width(), mDp, mPositionHelper);
+
+        // Apply thumbnail matrix
+        RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
+        float scale = mCurrentFullscreenParams.mScale;
+        float taskWidth = mTaskRect.width();
+        float taskHeight = mTaskRect.height();
+
+        mMatrix.set(mPositionHelper.getMatrix());
+        mMatrix.postScale(scale, scale);
+        mMatrix.postTranslate(insets.left, insets.top);
+
+        // Apply TaskView matrix: scale, translate, scroll
+        mMatrix.postScale(mCurveScale, mCurveScale, taskWidth / 2, taskHeight / 2);
+        mMatrix.postTranslate(mTaskRect.left, mTaskRect.top);
+        mOrientationState.getOrientationHandler().set(
+                mMatrix, MATRIX_POST_TRANSLATE, mScrollState.scroll);
+
+        // Apply recensView matrix
+        mMatrix.postScale(recentsViewScale.value, recentsViewScale.value, mPivot.x, mPivot.y);
+        postDisplayRotation(mOrientationState.getDisplayRotation(),
+                mDp.widthPx, mDp.heightPx, mMatrix);
+
+        // Crop rect is the inverse of thumbnail matrix
+        mTempRectF.set(-insets.left, -insets.top,
+                taskWidth + insets.right, taskHeight + insets.bottom);
+        mInversePositionMatrix.mapRect(mTempRectF);
+        mTempRectF.roundOut(mTmpCropRect);
+
+        SurfaceParams[] surfaceParams = new SurfaceParams[mAllTargets.unfilteredApps.length];
+        for (int i = 0; i < mAllTargets.unfilteredApps.length; i++) {
+            RemoteAnimationTargetCompat app = mAllTargets.unfilteredApps[i];
+            SurfaceParams.Builder builder = new SurfaceParams.Builder(app.leash)
+                    .withLayer(RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers));
+
+            if (app.mode == mAllTargets.targetMode) {
+                float alpha = mTaskAlphaCallback.getAlpha(app, params.getTargetAlpha());
+                if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
+                    // Fade out Assistant overlay.
+                    if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
+                            && app.isNotInRecents) {
+                        alpha = Interpolators.ACCEL_2.getInterpolation(fullScreenProgress.value);
+                    }
+
+                    builder.withAlpha(alpha)
+                            .withMatrix(mMatrix)
+                            .withWindowCrop(mTmpCropRect)
+                            .withCornerRadius(mCurrentFullscreenParams.mCurrentDrawnCornerRadius);
+                } else if (params.getTargetSet().hasRecents) {
+                    // If home has a different target then recents, reverse anim the home target.
+                    builder.withAlpha(fullScreenProgress.value * params.getTargetAlpha());
+                }
+            } else {
+                builder.withAlpha(1);
+                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.isLauncherOnTop()) {
+                    builder.withLayer(Integer.MAX_VALUE);
+                }
+            }
+            surfaceParams[i] = builder.build();
+        }
+
+        applySurfaceParams(params.getSyncTransactionApplier(), surfaceParams);
+    }
+
+    /**
+     * Interface for calculating taskSize
+     */
+    public interface TaskSizeProvider {
+
+        /**
+         * Sets the outRect to the expected taskSize
+         */
+        void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect);
+    }
+
+}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
index 78d75c5..454223e 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -183,27 +183,6 @@
         LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect);
     }
 
-    /**
-     * @return The translationX to apply to this view so that the first task is just offscreen.
-     */
-    public float getOffscreenTranslationX(float recentsScale) {
-        LauncherState.ScaleAndTranslation overviewScaleAndTranslation =
-            NORMAL.getOverviewScaleAndTranslation(mActivity);
-        float offscreen = mOrientationHandler.getTranslationValue(overviewScaleAndTranslation);
-        // Offset since scale pushes tasks outwards.
-        getTaskSize(sTempRect);
-        int taskSize = mOrientationHandler.getPrimarySize(sTempRect);
-        offscreen += taskSize * (recentsScale - 1) / 2;
-        if (mRunningTaskTileHidden) {
-            // The first task is hidden, so offset by its width.
-            offscreen -= (taskSize + getPageSpacing()) * recentsScale;
-        }
-        if (isRtl()) {
-            offscreen = -offscreen;
-        }
-        return offscreen;
-    }
-
     @Override
     protected void onTaskLaunchAnimationUpdate(float progress, TaskView tv) {
         if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
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 24eae18..b687920 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
@@ -56,6 +56,7 @@
 import android.content.res.Configuration;
 import android.graphics.Canvas;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Typeface;
@@ -90,7 +91,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
@@ -123,7 +123,6 @@
 import com.android.quickstep.TaskUtils;
 import com.android.quickstep.ViewUtils;
 import com.android.quickstep.util.AppWindowAnimationHelper;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.IPinnedStackAnimationListener;
@@ -175,8 +174,23 @@
                 }
             };
 
-    protected RecentsOrientedState mOrientationState;
+    public static final FloatProperty<RecentsView> ADJACENT_PAGE_OFFSET =
+            new FloatProperty<RecentsView>("adjacentPageOffset") {
+                @Override
+                public void setValue(RecentsView recentsView, float v) {
+                    if (recentsView.mAdjacentPageOffset != v) {
+                        recentsView.mAdjacentPageOffset = v;
+                        recentsView.updateAdjacentPageOffset();
+                    }
+                }
 
+                @Override
+                public Float get(RecentsView recentsView) {
+                    return recentsView.mAdjacentPageOffset;
+                }
+            };
+
+    protected final RecentsOrientedState mOrientationState;
     private OrientationEventListener mOrientationListener;
     private int mPreviousRotation;
     protected RecentsAnimationController mRecentsAnimationController;
@@ -188,6 +202,7 @@
     protected boolean mEnableDrawingLiveTile = false;
     protected final Rect mTempRect = new Rect();
     protected final RectF mTempRectF = new RectF();
+    private final PointF mTempPointF = new PointF();
 
     private static final int DISMISS_TASK_DURATION = 300;
     private static final int ADDITION_TASK_DURATION = 200;
@@ -198,7 +213,6 @@
     private final float mFastFlingVelocity;
     private final RecentsModel mModel;
     private final int mTaskTopMargin;
-    private final int mTaskBottomMargin;
     private final ClearAllButton mClearAllButton;
     private final Rect mClearAllButtonDeadZoneRect = new Rect();
     private final Rect mTaskViewDeadZoneRect = new Rect();
@@ -217,6 +231,8 @@
     private boolean mOverlayEnabled;
     protected boolean mFreezeViewVisibility;
 
+    private float mAdjacentPageOffset = 0;
+
     /**
      * TODO: Call reloadIdNeeded in onTaskStackChanged.
      */
@@ -285,6 +301,9 @@
         }
     };
 
+    private final RecentsOrientedState.SystemRotationChangeListener mSystemRotationChangeListener =
+            enabled -> toggleOrientationEventListener();
+
     private final PinnedStackAnimationListener mIPinnedStackAnimationListener =
             new PinnedStackAnimationListener();
 
@@ -349,7 +368,7 @@
 
         mFastFlingVelocity = getResources()
                 .getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
-        mActivity = (T) BaseActivity.fromContext(context);
+        mActivity = BaseActivity.fromContext(context);
         mModel = RecentsModel.INSTANCE.get(context);
         mIdp = InvariantDeviceProfile.INSTANCE.get(context);
         mTempAppWindowAnimationHelper =
@@ -365,7 +384,6 @@
         setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
         mTaskTopMargin = getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
-        mTaskBottomMargin = LayoutUtils.thumbnailBottomMargin(context);
         mSquaredTouchSlop = squaredTouchSlop(context);
 
         mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
@@ -484,6 +502,7 @@
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(
                 mIPinnedStackAnimationListener);
         mOrientationState.init();
+        mOrientationState.addSystemRotationChangeListener(mSystemRotationChangeListener);
     }
 
     @Override
@@ -498,6 +517,7 @@
         mIdp.removeOnChangeListener(this);
         SystemUiProxy.INSTANCE.get(getContext()).setPinnedStackAnimationListener(null);
         mIPinnedStackAnimationListener.setActivity(null);
+        mOrientationState.removeSystemRotationChangeListener(mSystemRotationChangeListener);
         mOrientationState.destroy();
     }
 
@@ -554,13 +574,6 @@
     }
 
     public void setOverviewStateEnabled(boolean enabled) {
-        if (canEnableOverviewRotationAnimation()) {
-            if (enabled) {
-                mOrientationListener.enable();
-            } else {
-                mOrientationListener.disable();
-            }
-        }
         mOverviewStateEnabled = enabled;
         updateTaskStackListenerState();
         if (!enabled) {
@@ -568,13 +581,26 @@
             // its thumbnail
             mTmpRunningTask = null;
         }
+        toggleOrientationEventListener();
+    }
+
+    private void toggleOrientationEventListener() {
+        boolean canEnable = canEnableOverviewRotationAnimation() && mOverviewStateEnabled;
+        UI_HELPER_EXECUTOR.execute(() -> {
+            if (canEnable) {
+                mOrientationListener.enable();
+            } else {
+                mOrientationListener.disable();
+            }
+        });
     }
 
     private boolean canEnableOverviewRotationAnimation() {
         return supportsVerticalLandscape() // not 3P launcher
                 && !TestProtocol.sDisableSensorRotation // Ignore hardware dependency for tests..
                 && mOrientationListener.canDetectOrientation() // ..but does the hardware even work?
-                && !mOrientationState.canLauncherAutoRotate(); // launcher is going to rotate itself
+                && (mOrientationState.isSystemRotationAllowed() &&
+                    !mOrientationState.canLauncherRotate()); // launcher is going to rotate itself
     }
 
     public void onDigitalWellbeingToastShown() {
@@ -819,7 +845,6 @@
         mTaskHeight = mTempRect.height();
 
         mTempRect.top -= mTaskTopMargin;
-        mTempRect.bottom += mTaskBottomMargin;
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
@@ -1073,7 +1098,7 @@
      */
     public void showCurrentTask(int runningTaskId) {
         if (getTaskView(runningTaskId) == null) {
-            boolean wasEmpty = getTaskViewCount() == 0;
+            boolean wasEmpty = getChildCount() == 0;
             // Add an empty view for now until the task plan is loaded and applied
             final TaskView taskView = mTaskViewPool.getView();
             addView(taskView, mTaskViewStartIndex);
@@ -1611,11 +1636,6 @@
     }
 
     @Nullable
-    public TaskView getPreviousTaskView() {
-        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
-    }
-
-    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAtByAbsoluteIndex(getCurrentPage());
     }
@@ -1674,11 +1694,29 @@
 
         updateEmptyStateUi(changed);
 
-        // Set the pivot points to match the task preview center
-        setPivotY(((mInsets.top + getPaddingTop() + mTaskTopMargin)
-                + (getHeight() - mInsets.bottom - getPaddingBottom() - mTaskBottomMargin)) / 2);
-        setPivotX(((mInsets.left + getPaddingLeft())
-                + (getWidth() - mInsets.right - getPaddingRight())) / 2);
+        // Update the pivots such that when the task is scaled, it fills the full page
+        getTaskSize(mTempRect);
+        getPagedViewOrientedState().getFullScreenScaleAndPivot(
+                mTempRect, mActivity.getDeviceProfile(), mTempPointF);
+        setPivotX(mTempPointF.x);
+        setPivotY(mTempPointF.y);
+        updateAdjacentPageOffset();
+    }
+
+    private void updateAdjacentPageOffset() {
+        float offset = mAdjacentPageOffset * getWidth();
+        if (mIsRtl) {
+            offset = -offset;
+        }
+        int count = getChildCount();
+
+        TaskView runningTask = mRunningTaskId == -1 ? null : getTaskView(mRunningTaskId);
+        int midPoint = runningTask == null ? -1 : indexOfChild(runningTask);
+
+        for (int i = 0; i < count; i++) {
+            getChildAt(i).setTranslationX(i == midPoint ? 0 : (i < midPoint ? -offset : offset));
+        }
+        updateCurveProperties();
     }
 
     private void updateDeadZoneRects() {
@@ -1760,14 +1798,10 @@
         int centerTaskIndex = getCurrentPage();
         boolean launchingCenterTask = taskIndex == centerTaskIndex;
 
-        LauncherState.ScaleAndTranslation toScaleAndTranslation = appWindowAnimationHelper
-                .getScaleAndTranslation();
-        float toScale = toScaleAndTranslation.scale;
-        float toTranslationY = toScaleAndTranslation.translationY;
+        float toScale = appWindowAnimationHelper.getSrcToTargetScale();
         if (launchingCenterTask) {
             RecentsView recentsView = tv.getRecentsView();
             anim.play(ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, toScale));
-            anim.play(ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y, toTranslationY));
             anim.play(ObjectAnimator.ofFloat(recentsView, FULLSCREEN_PROGRESS, 1));
         } else {
             // We are launching an adjacent task, so parallax the center and other adjacent task.
@@ -2027,17 +2061,23 @@
         return mClearAllButton;
     }
 
+
     /**
      * @return How many pixels the running task is offset on the x-axis due to the current scrollX.
      */
-    public float getScrollOffset() {
+    public int getScrollOffset() {
         if (getRunningTaskIndex() == -1) {
             return 0;
         }
-        int startScroll = getScrollForPage(getRunningTaskIndex());
-        int offsetX = startScroll - mOrientationHandler.getPrimaryScroll(this);
-        offsetX *= mOrientationHandler.getPrimaryScale(this);
-        return offsetX;
+        return getScrollForPage(getRunningTaskIndex()) - mOrientationHandler.getPrimaryScroll(this);
+    }
+
+    /**
+     * @return How many pixels the running task is offset on the x-axis due to the current scrollX
+     * and parent scale.
+     */
+    public float getScrollOffsetScaled() {
+        return getScrollOffset() * mOrientationHandler.getPrimaryScale(this);
     }
 
     public Consumer<MotionEvent> getEventDispatcher(float navbarRotation) {
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
index 178ff32..a05e0fa 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -36,11 +36,9 @@
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
-import android.util.Log;
 import android.util.Property;
 import android.view.Surface;
 import android.view.View;
-import android.view.ViewGroup;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.R;
@@ -50,7 +48,7 @@
 import com.android.launcher3.util.Themes;
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskOverlayFactory.TaskOverlay;
-import com.android.quickstep.util.TaskCornerRadius;
+import com.android.quickstep.views.TaskView.FullscreenDrawParams;
 import com.android.systemui.plugins.OverviewScreenshotActions;
 import com.android.systemui.plugins.PluginListener;
 import com.android.systemui.shared.recents.model.Task;
@@ -66,6 +64,8 @@
     private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix();
     private static final RectF EMPTY_RECT_F = new RectF();
 
+    private static final FullscreenDrawParams TEMP_PARAMS = new FullscreenDrawParams();
+
     public static final Property<TaskThumbnailView, Float> DIM_ALPHA =
             new FloatProperty<TaskThumbnailView>("dimAlpha") {
                 @Override
@@ -87,12 +87,11 @@
     private final Paint mClearPaint = new Paint();
     private final Paint mDimmingPaintAfterClearing = new Paint();
 
-    private final Matrix mMatrix = new Matrix();
-
-    private float mClipBottom = -1;
     // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
-    private RectF mClippedInsets = new RectF();
-    private TaskView.FullscreenDrawParams mFullscreenParams;
+    private final Rect mPreviewRect = new Rect();
+    private final PreviewPositionHelper mPreviewPositionHelper;
+    // Initialize with dummy value. It is overridden later by TaskView
+    private TaskView.FullscreenDrawParams mFullscreenParams = TEMP_PARAMS;
 
     private Task mTask;
     private ThumbnailData mThumbnailData;
@@ -103,7 +102,6 @@
     private float mSaturation = 1f;
 
     private boolean mOverlayEnabled;
-    private boolean mIsOrientationChanged;
     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
     public TaskThumbnailView(Context context) {
@@ -123,7 +121,7 @@
         mDimmingPaintAfterClearing.setColor(Color.BLACK);
         mActivity = BaseActivity.fromContext(context);
         mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText);
-        mFullscreenParams = new TaskView.FullscreenDrawParams(TaskCornerRadius.get(context));
+        mPreviewPositionHelper = new PreviewPositionHelper(context);
     }
 
     public void bind(Task task) {
@@ -172,8 +170,7 @@
             mOverlay.reset();
         }
         if (mOverviewScreenshotActionsPlugin != null) {
-            mOverviewScreenshotActionsPlugin
-                .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity);
+            mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity);
         }
         updateThumbnailPaintFilter();
     }
@@ -270,9 +267,8 @@
         PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this);
     }
 
-    public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
-        // Don't show insets in multi window mode.
-        return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
+    public PreviewPositionHelper getPreviewPositionHelper() {
+        return mPreviewPositionHelper;
     }
 
     public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) {
@@ -294,16 +290,17 @@
         // Draw the background in all cases, except when the thumbnail data is opaque
         final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null
                 || mThumbnailData == null;
-        if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) {
+        if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0
+                || mThumbnailData.isTranslucent) {
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint);
             if (drawBackgroundOnly) {
                 return;
             }
         }
 
-        if (mClipBottom > 0) {
+        if (mPreviewPositionHelper.mClipBottom > 0) {
             canvas.save();
-            canvas.clipRect(x, y, width, mClipBottom);
+            canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom);
             canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint);
             canvas.restore();
         } else {
@@ -324,8 +321,9 @@
 
     private void updateOverlay() {
         // The overlay doesn't really work when the screenshot is rotated, so don't add it.
-        if (mOverlayEnabled && !mIsOrientationChanged && mBitmapShader != null && mThumbnailData != null) {
-            mOverlay.initOverlay(mTask, mThumbnailData, mMatrix);
+        if (mOverlayEnabled && !mPreviewPositionHelper.mIsOrientationChanged
+                && mBitmapShader != null && mThumbnailData != null) {
+            mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix);
         } else {
             mOverlay.reset();
         }
@@ -346,76 +344,17 @@
     }
 
     private void updateThumbnailMatrix() {
-        boolean isRotated = false;
-        boolean isOrientationDifferent = false;
-        mClipBottom = -1;
+        mPreviewPositionHelper.mClipBottom = -1;
+        mPreviewPositionHelper.mIsOrientationChanged = false;
         if (mBitmapShader != null && mThumbnailData != null) {
-            float scale = mThumbnailData.scale;
-            Rect thumbnailInsets = mThumbnailData.insets;
-            final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() -
-                    (thumbnailInsets.left + thumbnailInsets.right) * scale;
-            final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() -
-                    (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+            mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(),
+                    mThumbnailData.thumbnail.getHeight());
+            mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData,
+                    mActivity.isInMultiWindowMode(), getMeasuredWidth(), getMeasuredHeight());
 
-            final float thumbnailScale;
-            int thumbnailRotation = mThumbnailData.rotation;
-            int currentRotation = ConfigurationCompat.getWindowConfigurationRotation(
-                    getResources().getConfiguration());
-            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
-            // Landscape vs portrait change
-            boolean windowingModeSupportsRotation = !mActivity.isInMultiWindowMode()
-                    && mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
-            isOrientationDifferent = isOrientationChange(deltaRotate)
-                && windowingModeSupportsRotation;
-            if (getMeasuredWidth() == 0) {
-                // If we haven't measured , skip the thumbnail drawing and only draw the background
-                // color
-                thumbnailScale = 0f;
-            } else {
-                // Rotate the screenshot if not in multi-window mode
-                isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
-                // Scale the screenshot to always fit the width of the card.
-
-                thumbnailScale = isOrientationDifferent
-                        ? getMeasuredWidth() / thumbnailHeight
-                        : getMeasuredWidth() / thumbnailWidth;
-            }
-
-            if (!isRotated) {
-                // No Rotation
-                mClippedInsets.offsetTo(thumbnailInsets.left * scale,
-                    thumbnailInsets.top * scale);
-                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
-            } else {
-                setThumbnailRotation(deltaRotate, thumbnailInsets, scale);
-            }
-
-            final float widthWithInsets;
-            final float heightWithInsets;
-            if (isOrientationDifferent) {
-                widthWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-            } else {
-                widthWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale;
-                heightWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale;
-            }
-            mClippedInsets.left *= thumbnailScale;
-            mClippedInsets.top *= thumbnailScale;
-            mClippedInsets.right = widthWithInsets - mClippedInsets.left - getMeasuredWidth();
-            mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - getMeasuredHeight();
-
-            mMatrix.postScale(thumbnailScale, thumbnailScale);
-            mBitmapShader.setLocalMatrix(mMatrix);
-
-            float bitmapHeight = Math.max((isOrientationDifferent ? thumbnailWidth : thumbnailHeight)
-                    * thumbnailScale, 0);
-            if (Math.round(bitmapHeight) < getMeasuredHeight()) {
-                mClipBottom = bitmapHeight;
-            }
+            mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix);
             mPaint.setShader(mBitmapShader);
         }
-
-        mIsOrientationChanged = isOrientationDifferent;
         invalidate();
 
         // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay
@@ -423,51 +362,6 @@
         post(this::updateOverlay);
     }
 
-    private int getRotationDelta(int oldRotation, int newRotation) {
-        int delta = newRotation - oldRotation;
-        if (delta < 0) delta += 4;
-        return delta;
-    }
-
-    /**
-     * @param deltaRotation the number of 90 degree turns from the current orientation
-     * @return {@code true} if the change in rotation results in a shift from landscape to portrait
-     * or vice versa, {@code false} otherwise
-     */
-    private boolean isOrientationChange(int deltaRotation) {
-        return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
-    }
-
-    private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale) {
-        int newLeftInset = 0;
-        int newTopInset = 0;
-        int translateX = 0;
-        int translateY = 0;
-
-        mMatrix.setRotate(90 * deltaRotate);
-        switch (deltaRotate) { /* Counter-clockwise */
-            case Surface.ROTATION_90:
-                newLeftInset = thumbnailInsets.bottom;
-                newTopInset = thumbnailInsets.left;
-                translateX = mThumbnailData.thumbnail.getHeight();
-                break;
-            case Surface.ROTATION_270:
-                newLeftInset = thumbnailInsets.top;
-                newTopInset = thumbnailInsets.right;
-                translateY = mThumbnailData.thumbnail.getWidth();
-                break;
-            case Surface.ROTATION_180:
-                newLeftInset = -thumbnailInsets.top;
-                newTopInset = -thumbnailInsets.left;
-                translateX = mThumbnailData.thumbnail.getWidth();
-                translateY = mThumbnailData.thumbnail.getHeight();
-                break;
-        }
-        mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
-        mMatrix.postTranslate(translateX - mClippedInsets.left,
-                translateY - mClippedInsets.top);
-    }
-
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
@@ -511,4 +405,158 @@
         }
         return mThumbnailData.thumbnail;
     }
+
+    /**
+     * Utility class to position the thumbnail in the TaskView
+     */
+    public static class PreviewPositionHelper {
+
+        // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+        private final RectF mClippedInsets = new RectF();
+        private final Matrix mMatrix = new Matrix();
+        private float mClipBottom = -1;
+        private boolean mIsOrientationChanged;
+
+        private final Context mContext;
+
+        public PreviewPositionHelper(Context context) {
+            mContext = context;
+        }
+
+        public int getCurrentRotation() {
+            return ConfigurationCompat.getWindowConfigurationRotation(
+                    mContext.getResources().getConfiguration());
+        }
+
+        public Matrix getMatrix() {
+            return mMatrix;
+        }
+
+        /**
+         * Updates the matrix based on the provided parameters
+         */
+        public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData,
+                boolean isInMultiWindowMode, int canvasWidth, int canvasHeight) {
+            boolean isRotated = false;
+            boolean isOrientationDifferent;
+            mClipBottom = -1;
+
+            float scale = thumbnailData.scale;
+            Rect thumbnailInsets = thumbnailData.insets;
+            final float thumbnailWidth = thumbnailPosition.width()
+                    - (thumbnailInsets.left + thumbnailInsets.right) * scale;
+            final float thumbnailHeight = thumbnailPosition.height()
+                    - (thumbnailInsets.top + thumbnailInsets.bottom) * scale;
+
+            final float thumbnailScale;
+            int thumbnailRotation = thumbnailData.rotation;
+            int currentRotation = getCurrentRotation();
+            int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation);
+
+            // Landscape vs portrait change
+            boolean windowingModeSupportsRotation = !isInMultiWindowMode
+                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+            isOrientationDifferent = isOrientationChange(deltaRotate)
+                    && windowingModeSupportsRotation;
+            if (canvasWidth == 0) {
+                // If we haven't measured , skip the thumbnail drawing and only draw the background
+                // color
+                thumbnailScale = 0f;
+            } else {
+                // Rotate the screenshot if not in multi-window mode
+                isRotated = deltaRotate > 0 && windowingModeSupportsRotation;
+                // Scale the screenshot to always fit the width of the card.
+                thumbnailScale = isOrientationDifferent
+                        ? canvasWidth / thumbnailHeight
+                        : canvasWidth / thumbnailWidth;
+            }
+
+            if (!isRotated) {
+                // No Rotation
+                mClippedInsets.offsetTo(thumbnailInsets.left * scale,
+                        thumbnailInsets.top * scale);
+                mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top);
+            } else {
+                setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition);
+            }
+            mMatrix.postTranslate(-thumbnailPosition.left, -thumbnailPosition.top);
+
+            final float widthWithInsets;
+            final float heightWithInsets;
+            if (isOrientationDifferent) {
+                widthWithInsets = thumbnailPosition.height() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.width() * thumbnailScale;
+            } else {
+                widthWithInsets = thumbnailPosition.width() * thumbnailScale;
+                heightWithInsets = thumbnailPosition.height() * thumbnailScale;
+            }
+            mClippedInsets.left *= thumbnailScale;
+            mClippedInsets.top *= thumbnailScale;
+            mClippedInsets.right = widthWithInsets - mClippedInsets.left - canvasWidth;
+            mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - canvasHeight;
+
+            mMatrix.postScale(thumbnailScale, thumbnailScale);
+
+            float bitmapHeight = Math.max(0,
+                    (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale);
+            if (Math.round(bitmapHeight) < canvasHeight) {
+                mClipBottom = bitmapHeight;
+            }
+            mIsOrientationChanged = isOrientationDifferent;
+        }
+
+        private int getRotationDelta(int oldRotation, int newRotation) {
+            int delta = newRotation - oldRotation;
+            if (delta < 0) delta += 4;
+            return delta;
+        }
+
+        /**
+         * @param deltaRotation the number of 90 degree turns from the current orientation
+         * @return {@code true} if the change in rotation results in a shift from landscape to
+         * portrait or vice versa, {@code false} otherwise
+         */
+        private boolean isOrientationChange(int deltaRotation) {
+            return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270;
+        }
+
+        private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale,
+                Rect thumbnailPosition) {
+            int newLeftInset = 0;
+            int newTopInset = 0;
+            int translateX = 0;
+            int translateY = 0;
+
+            mMatrix.setRotate(90 * deltaRotate);
+            switch (deltaRotate) { /* Counter-clockwise */
+                case Surface.ROTATION_90:
+                    newLeftInset = thumbnailInsets.bottom;
+                    newTopInset = thumbnailInsets.left;
+                    translateX = thumbnailPosition.height();
+                    break;
+                case Surface.ROTATION_270:
+                    newLeftInset = thumbnailInsets.top;
+                    newTopInset = thumbnailInsets.right;
+                    translateY = thumbnailPosition.width();
+                    break;
+                case Surface.ROTATION_180:
+                    newLeftInset = -thumbnailInsets.top;
+                    newTopInset = -thumbnailInsets.left;
+                    translateX = thumbnailPosition.width();
+                    translateY = thumbnailPosition.height();
+                    break;
+            }
+            mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
+            mMatrix.postTranslate(translateX - mClippedInsets.left,
+                    translateY - mClippedInsets.top);
+        }
+
+        /**
+         * Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
+         */
+        public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) {
+            // Don't show insets in multi window mode.
+            return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets;
+        }
+    }
 }
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 470b720..b0758c9 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
@@ -56,6 +56,7 @@
 import android.widget.Toast;
 
 import com.android.launcher3.BaseDraggingActivity;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -77,11 +78,11 @@
 import com.android.quickstep.TaskOverlayFactory;
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.TaskUtils;
-import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
 import com.android.quickstep.util.TaskCornerRadius;
 import com.android.quickstep.views.RecentsView.PageCallbacks;
 import com.android.quickstep.views.RecentsView.ScrollState;
+import com.android.quickstep.views.TaskThumbnailView.PreviewPositionHelper;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.ActivityOptionsCompat;
@@ -157,8 +158,6 @@
     private float mCurveScale;
     private float mFullscreenProgress;
     private final FullscreenDrawParams mCurrentFullscreenParams;
-    private final float mCornerRadius;
-    private final float mWindowCornerRadius;
     private final BaseDraggingActivity mActivity;
 
     private ObjectAnimator mIconAndDimAnimator;
@@ -211,9 +210,8 @@
                     TaskUtils.getLaunchComponentKeyForTask(getTask().key));
             mActivity.getStatsLogManager().log(TASK_LAUNCH_TAP, buildProto());
         });
-        mCornerRadius = TaskCornerRadius.get(context);
-        mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
-        mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
+
+        mCurrentFullscreenParams = new FullscreenDrawParams(context);
         mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
 
         mOutlineProvider = new TaskOutlineProvider(getContext(), mCurrentFullscreenParams);
@@ -236,11 +234,6 @@
         super.onFinishInflate();
         mSnapshotView = findViewById(R.id.snapshot);
         mIconView = findViewById(R.id.icon);
-        final Context context = getContext();
-
-        TaskView.LayoutParams thumbnailParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        thumbnailParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(context);
-        mSnapshotView.setLayoutParams(thumbnailParams);
     }
 
     public boolean isTaskOverlayModal() {
@@ -473,8 +466,6 @@
         int iconRotation = orientationState.getTouchRotation();
         PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler();
         boolean isRtl = orientationHandler.getRecentsRtlSetting(getResources());
-        LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
-        snapshotParams.bottomMargin = LayoutUtils.thumbnailBottomMargin(getContext());
         int thumbnailPadding = (int) getResources().getDimension(R.dimen.task_thumbnail_top_margin);
         LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams();
         int rotation = orientationState.getTouchRotationDegrees();
@@ -501,7 +492,6 @@
                     iconParams.bottomMargin = 0;
                 break;
         }
-        mSnapshotView.setLayoutParams(snapshotParams);
         mIconView.setLayoutParams(iconParams);
         mIconView.setRotation(rotation);
     }
@@ -699,21 +689,16 @@
         return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
     }
 
-    public void setCurveScale(float curveScale) {
+    private void setCurveScale(float curveScale) {
         mCurveScale = curveScale;
-        onScaleChanged();
+        setScaleX(mCurveScale);
+        setScaleY(mCurveScale);
     }
 
     public float getCurveScale() {
         return mCurveScale;
     }
 
-    private void onScaleChanged() {
-        float scale = mCurveScale;
-        setScaleX(scale);
-        setScaleY(scale);
-    }
-
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -723,13 +708,11 @@
     private static final class TaskOutlineProvider extends ViewOutlineProvider {
 
         private final int mMarginTop;
-        private final int mMarginBottom;
         private FullscreenDrawParams mFullscreenParams;
 
         TaskOutlineProvider(Context context, FullscreenDrawParams fullscreenParams) {
             mMarginTop = context.getResources().getDimensionPixelSize(
                     R.dimen.task_thumbnail_top_margin);
-            mMarginBottom = LayoutUtils.thumbnailBottomMargin(context);
             mFullscreenParams = fullscreenParams;
         }
 
@@ -744,7 +727,7 @@
             outline.setRoundRect(0,
                     (int) (mMarginTop * scale),
                     (int) ((insets.left + view.getWidth() + insets.right) * scale),
-                    (int) ((insets.top + view.getHeight() + insets.bottom - mMarginBottom) * scale),
+                    (int) ((insets.top + view.getHeight() + insets.bottom) * scale),
                     mFullscreenParams.mCurrentDrawnCornerRadius);
         }
     }
@@ -917,23 +900,11 @@
         setClipToPadding(!isFullscreen);
 
         TaskThumbnailView thumbnail = getThumbnail();
-        boolean isMultiWindowMode = mActivity.getDeviceProfile().isMultiWindowMode;
-        RectF insets = thumbnail.getInsetsToDrawInFullscreen(isMultiWindowMode);
-        float currentInsetsLeft = insets.left * mFullscreenProgress;
-        float currentInsetsRight = insets.right * mFullscreenProgress;
-        mCurrentFullscreenParams.setInsets(currentInsetsLeft,
-                insets.top * mFullscreenProgress,
-                currentInsetsRight,
-                insets.bottom * mFullscreenProgress);
-        float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius;
-        mCurrentFullscreenParams.setCornerRadius(Utilities.mapRange(mFullscreenProgress,
-                mCornerRadius, fullscreenCornerRadius) / getRecentsView().getScaleX());
-        // We scaled the thumbnail to fit the content (excluding insets) within task view width.
-        // Now that we are drawing left/right insets again, we need to scale down to fit them.
-        if (getWidth() > 0) {
-            mCurrentFullscreenParams.setScale(getWidth()
-                    / (getWidth() + currentInsetsLeft + currentInsetsRight));
-        }
+        mCurrentFullscreenParams.setProgress(
+                mFullscreenProgress,
+                getRecentsView().getScaleX(),
+                getWidth(), mActivity.getDeviceProfile(),
+                thumbnail.getPreviewPositionHelper());
 
         if (!getRecentsView().isTaskIconScaledDown(this)) {
             // Some of the items in here are dependent on the current fullscreen params, but don't
@@ -971,26 +942,51 @@
     /**
      * We update and subsequently draw these in {@link #setFullscreenProgress(float)}.
      */
-    static class FullscreenDrawParams {
-        RectF mCurrentDrawnInsets = new RectF();
-        float mCurrentDrawnCornerRadius;
+    public static class FullscreenDrawParams {
+
+        private final float mCornerRadius;
+        private final float mWindowCornerRadius;
+
+        public RectF mCurrentDrawnInsets = new RectF();
+        public float mCurrentDrawnCornerRadius;
         /** The current scale we apply to the thumbnail to adjust for new left/right insets. */
-        float mScale = 1;
+        public float mScale = 1;
 
-        public FullscreenDrawParams(float cornerRadius) {
-            setCornerRadius(cornerRadius);
+        public FullscreenDrawParams(Context context) {
+            mCornerRadius = TaskCornerRadius.get(context);
+            mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
+
+            mCurrentDrawnCornerRadius = mCornerRadius;
         }
 
-        public void setInsets(float left, float top, float right, float bottom) {
-            mCurrentDrawnInsets.set(left, top, right, bottom);
+        public FullscreenDrawParams() {
+            mCurrentDrawnCornerRadius = mWindowCornerRadius =  mCornerRadius = 0;
         }
 
-        public void setCornerRadius(float cornerRadius) {
-            mCurrentDrawnCornerRadius = cornerRadius;
+        /**
+         * Sets the progress in range [0, 1]
+         */
+        public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
+                DeviceProfile dp, PreviewPositionHelper pph) {
+            boolean isMultiWindowMode = dp.isMultiWindowMode;
+            RectF insets = pph.getInsetsToDrawInFullscreen(isMultiWindowMode);
+
+            float currentInsetsLeft = insets.left * fullscreenProgress;
+            float currentInsetsRight = insets.right * fullscreenProgress;
+            mCurrentDrawnInsets.set(currentInsetsLeft, insets.top * fullscreenProgress,
+                    currentInsetsRight, insets.bottom * fullscreenProgress);
+            float fullscreenCornerRadius = isMultiWindowMode ? 0 : mWindowCornerRadius;
+
+            mCurrentDrawnCornerRadius =
+                    Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
+                            / parentScale;
+
+            // We scaled the thumbnail to fit the content (excluding insets) within task view width.
+            // Now that we are drawing left/right insets again, we need to scale down to fit them.
+            if (previewWidth > 0) {
+                mScale = previewWidth / (previewWidth + currentInsetsLeft + currentInsetsRight);
+            }
         }
 
-        public void setScale(float scale) {
-            mScale = scale;
-        }
     }
 }
diff --git a/quickstep/res/values/config.xml b/quickstep/res/values/config.xml
index a688f9a..2b11ca0 100644
--- a/quickstep/res/values/config.xml
+++ b/quickstep/res/values/config.xml
@@ -13,13 +13,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources xmlns:tools="http://schemas.android.com/tools">
+<resources>
     <string name="task_overlay_factory_class" translatable="false"/>
 
     <!-- Activities which block home gesture -->
-    <string-array name="gesture_blocking_activities" tools:ignore="InconsistentArrays">
-        <item>com.android.launcher3/com.android.quickstep.interaction.GestureSandboxActivity</item>
-    </string-array>
+    <string-array name="gesture_blocking_activities" translatable="false"/>
 
     <string name="stats_log_manager_class" translatable="false">com.android.quickstep.logging.StatsLogCompatManager</string>
 
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index b7abd61..65763d4 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -29,7 +29,6 @@
 import android.os.Bundle;
 import android.os.CancellationSignal;
 
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.WellbeingModel;
@@ -38,7 +37,6 @@
 import com.android.launcher3.proxy.StartActivityParams;
 import com.android.launcher3.statehandlers.BackButtonAlphaHandler;
 import com.android.launcher3.statehandlers.DepthController;
-import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.OnboardingPrefs;
 import com.android.launcher3.util.UiThreadHelper;
@@ -205,17 +203,6 @@
     }
 
     @Override
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        if (SysUINavigationMode.getMode(this) == Mode.NO_BUTTON) {
-            PagedOrientationHandler layoutVertical =
-                ((RecentsView)getOverviewPanel()).getPagedViewOrientedState().getOrientationHandler();
-            return layoutVertical.getScaleAndTranslation(getDeviceProfile(),
-                getOverviewPanel());
-        }
-        return super.getOverviewScaleAndTranslationForNormalState();
-    }
-
-    @Override
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) {
         QuickstepAppTransitionManagerImpl appTransitionManager =
                 (QuickstepAppTransitionManagerImpl) getAppTransitionManager();
@@ -238,6 +225,12 @@
     }
 
     @Override
+    public float[] getNormalOverviewScaleAndOffset() {
+        return SysUINavigationMode.getMode(this) == Mode.NO_BUTTON
+                ? new float[] {1, 1} : new float[] {1.1f, 0};
+    }
+
+    @Override
     public void onDragLayerHierarchyChanged() {
         onLauncherStateOrFocusChanged();
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 0f45196..33011ac 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -17,8 +17,6 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
-import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
 import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.graphics.Scrim.SCRIM_PROGRESS;
@@ -26,23 +24,22 @@
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCRIM_FADE;
 import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_X;
-import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_TRANSLATE_Y;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_PEEK;
 import static com.android.launcher3.states.StateAnimationConfig.PLAY_ATOMIC_OVERVIEW_SCALE;
 import static com.android.launcher3.states.StateAnimationConfig.SKIP_OVERVIEW;
+import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
 
 import android.util.FloatProperty;
-import android.view.View;
 
 import androidx.annotation.NonNull;
 
 import com.android.launcher3.BaseQuickstepLauncher;
 import com.android.launcher3.LauncherState;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.states.StateAnimationConfig;
+import com.android.quickstep.views.RecentsView;
 
 /**
  * State handler for recents view. Manages UI changes and animations for recents view based off the
@@ -50,7 +47,7 @@
  *
  * @param <T> the recents view
  */
-public abstract class BaseRecentsViewStateController<T extends View>
+public abstract class BaseRecentsViewStateController<T extends RecentsView>
         implements StateHandler {
     protected final T mRecentsView;
     protected final BaseQuickstepLauncher mLauncher;
@@ -62,14 +59,9 @@
 
     @Override
     public void setState(@NonNull LauncherState state) {
-        ScaleAndTranslation scaleAndTranslation = state.getOverviewScaleAndTranslation(mLauncher);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        SCALE_PROPERTY.set(mRecentsView, scaleAndTranslation.scale);
-        mRecentsView.setTranslationX(translationX);
-        mRecentsView.setTranslationY(scaleAndTranslation.translationY);
+        float[] scaleAndOffset = state.getOverviewScaleAndOffset(mLauncher);
+        SCALE_PROPERTY.set(mRecentsView, scaleAndOffset[0]);
+        ADJACENT_PAGE_OFFSET.set(mRecentsView, scaleAndOffset[1]);
 
         getContentAlphaProperty().set(mRecentsView, state.overviewUi ? 1f : 0);
         OverviewScrim scrim = mLauncher.getDragLayer().getOverviewScrim();
@@ -98,17 +90,11 @@
      */
     void setStateWithAnimationInternal(@NonNull final LauncherState toState,
             @NonNull StateAnimationConfig config, @NonNull PendingAnimation setter) {
-        ScaleAndTranslation scaleAndTranslation = toState.getOverviewScaleAndTranslation(mLauncher);
-        float translationX = scaleAndTranslation.translationX;
-        if (mRecentsView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
-            translationX = -translationX;
-        }
-        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndTranslation.scale,
+        float[] scaleAndOffset = toState.getOverviewScaleAndOffset(mLauncher);
+        setter.setFloat(mRecentsView, SCALE_PROPERTY, scaleAndOffset[0],
                 config.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_X, translationX,
+        setter.setFloat(mRecentsView, ADJACENT_PAGE_OFFSET, scaleAndOffset[1],
                 config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_X, LINEAR));
-        setter.setFloat(mRecentsView, VIEW_TRANSLATE_Y, scaleAndTranslation.translationY,
-                config.getInterpolator(ANIM_OVERVIEW_TRANSLATE_Y, LINEAR));
 
         setter.setFloat(mRecentsView, getContentAlphaProperty(), toState.overviewUi ? 1 : 0,
                 config.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
index 93e02a1..ea71d97 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -87,7 +87,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 1f;
     }
 
@@ -102,9 +102,8 @@
     }
 
     @Override
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        float slightParallax = -launcher.getDeviceProfile().allAppsCellHeightPx * 0.3f;
-        return new ScaleAndTranslation(0.9f, 0f, slightParallax);
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return new float[] {0.9f, 0};
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 94ef15a..e4bb9aa 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -135,8 +135,6 @@
 
         void createActivityInterface(long transitionLength);
 
-        default void adjustActivityControllerInterpolators() { }
-
         default void onTransitionCancelled() { }
 
         default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
diff --git a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
index 2a0fe32..dfb8c1d 100644
--- a/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
+++ b/quickstep/src/com/android/quickstep/util/ActivityInitListener.java
@@ -28,7 +28,7 @@
 
 public class ActivityInitListener<T extends BaseActivity> implements SchedulerCallback<T> {
 
-    private final BiPredicate<T, Boolean> mOnInitListener;
+    private BiPredicate<T, Boolean> mOnInitListener;
     private final ActivityTracker<T> mActivityTracker;
 
     private boolean mIsRegistered = false;
@@ -72,6 +72,7 @@
      */
     public void unregister() {
         mIsRegistered = false;
+        mOnInitListener = null;
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
index 1f1a999..4edf2fb 100644
--- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java
+++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java
@@ -63,7 +63,7 @@
             if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
                 //TODO: this needs to account for the swipe gesture height and accessibility
                 // UI when shown.
-                extraSpace = 0;
+                extraSpace = res.getDimensionPixelSize(R.dimen.overview_actions_height);
             } else {
                 extraSpace = getDefaultSwipeHeight(context, dp) + dp.workspacePageIndicatorHeight
                         + res.getDimensionPixelSize(
@@ -75,7 +75,14 @@
     }
 
     public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) {
-        calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect);
+        float extraSpace;
+        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
+            extraSpace = context.getResources()
+                    .getDimensionPixelSize(R.dimen.overview_actions_height);
+        } else {
+            extraSpace = 0;
+        }
+        calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect);
     }
 
     @AnyThread
@@ -123,8 +130,6 @@
         }
 
         float topIconMargin = res.getDimension(R.dimen.task_thumbnail_top_margin);
-        float bottomMargin = thumbnailBottomMargin(context);
-
         float paddingVert = overviewActionsEnabled && removeShelfFromOverview(context)
                 ? 0 : res.getDimension(R.dimen.task_card_vert_space);
 
@@ -134,7 +139,7 @@
         int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
 
         float availableHeight = launcherVisibleHeight
-                - topIconMargin - extraVerticalSpace - paddingVert - bottomMargin;
+                - topIconMargin - extraVerticalSpace - paddingVert;
         float availableWidth = launcherVisibleWidth - paddingHorz;
 
         float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
@@ -144,7 +149,7 @@
         // Center in the visible space
         float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
         float y = insets.top + Math.max(topIconMargin,
-                (launcherVisibleHeight - extraVerticalSpace - outHeight - bottomMargin) / 2);
+                (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
         outRect.set(Math.round(x), Math.round(y),
                 Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
     }
@@ -163,14 +168,16 @@
     }
 
     /**
-     * Get the margin that the task thumbnail view should use.
-     * @return the margin in pixels.
+     * Gets the scale that should be applied to the TaskView so that it matches the target
      */
-    public static int thumbnailBottomMargin(Context context) {
-        if (ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context)) {
-            return context.getResources().getDimensionPixelSize(R.dimen.overview_actions_height);
+    public static float getTaskScale(RecentsOrientedState orientedState,
+            float srcWidth, float srcHeight, float targetWidth, float targetHeight) {
+        if (orientedState == null
+                || orientedState.isHomeRotationAllowed()
+                || orientedState.isDisplayPhoneNatural()) {
+            return srcWidth / targetWidth;
         } else {
-            return 0;
+            return srcHeight / targetHeight;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
index eefe8ac..5be0675 100644
--- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
+++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java
@@ -18,9 +18,9 @@
 
 import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.hardware.camera2.params.OutputConfiguration.ROTATION_180;
 import static android.util.DisplayMetrics.DENSITY_DEVICE_STABLE;
 import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
 import static android.view.Surface.ROTATION_270;
 import static android.view.Surface.ROTATION_90;
 
@@ -28,6 +28,7 @@
 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
 import static com.android.launcher3.states.RotationHelper.FIXED_ROTATION_TRANSFORM_SETTING_NAME;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
 import android.content.ContentResolver;
@@ -36,6 +37,8 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Handler;
 import android.provider.Settings;
@@ -45,12 +48,15 @@
 
 import androidx.annotation.IntDef;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.PortraitPagedViewHandler;
 
 import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Container to hold orientation/rotation related information for Launcher.
@@ -81,6 +87,10 @@
     private @SurfaceRotation int mDisplayRotation = ROTATION_0;
     private @SurfaceRotation int mLauncherRotation = Surface.ROTATION_0;
 
+    public interface SystemRotationChangeListener {
+        void onSystemRotationChanged(boolean enabled);
+    }
+
     /**
      * If {@code true} we default to {@link PortraitPagedViewHandler} and don't support any fake
      * launcher orientations.
@@ -93,6 +103,7 @@
     private final SharedPreferences mSharedPrefs;
     private final boolean mAllowConfigurationDefaultValue;
 
+    private List<SystemRotationChangeListener> mSystemRotationChangeListeners = new ArrayList<>();
 
     private final Matrix mTmpMatrix = new Matrix();
     private final Matrix mTmpInverseMatrix = new Matrix();
@@ -128,7 +139,7 @@
      */
     public boolean update(
             @SurfaceRotation int touchRotation, @SurfaceRotation int displayRotation,
-            int launcherRotation) {
+            @SurfaceRotation int launcherRotation) {
         if (!FeatureFlags.ENABLE_FIXED_ROTATION_TRANSFORM.get()) {
             return false;
         }
@@ -167,14 +178,6 @@
         return true;
     }
 
-    public boolean areMultipleLayoutOrientationsDisabled() {
-        return mDisableMultipleOrientations;
-    }
-
-    public boolean canLauncherAutoRotate() {
-        return mIsHomeRotationAllowed && mIsSystemRotationAllowed;
-    }
-
     /**
      * Setting this preference renders future calls to {@link #update(int, int, int)} as a no-op.
      */
@@ -198,6 +201,10 @@
         } catch (Settings.SettingNotFoundException e) {
             Log.e(TAG, "autorotate setting not found", e);
         }
+
+        for (SystemRotationChangeListener listener : mSystemRotationChangeListeners) {
+            listener.onSystemRotationChanged(mIsSystemRotationAllowed);
+        }
     }
 
     private void updateHomeRotationSetting() {
@@ -205,6 +212,15 @@
                 mAllowConfigurationDefaultValue);
     }
 
+    public void addSystemRotationChangeListener(SystemRotationChangeListener listener) {
+        mSystemRotationChangeListeners.add(listener);
+        listener.onSystemRotationChanged(mIsSystemRotationAllowed);
+    }
+
+    public void removeSystemRotationChangeListener(SystemRotationChangeListener listener) {
+        mSystemRotationChangeListeners.remove(listener);
+    }
+
     public void init() {
         mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
         mContentResolver.registerContentObserver(
@@ -217,6 +233,7 @@
     public void destroy() {
         mSharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
         mContentResolver.unregisterContentObserver(mSystemAutoRotateObserver);
+        mSystemRotationChangeListeners.clear();
     }
 
     @SurfaceRotation
@@ -229,12 +246,25 @@
         return mTouchRotation;
     }
 
+    @SurfaceRotation
+    public int getLauncherRotation() {
+        return mLauncherRotation;
+    }
+
+    public boolean areMultipleLayoutOrientationsDisabled() {
+        return mDisableMultipleOrientations;
+    }
+
+    public boolean isSystemRotationAllowed() {
+        return mIsSystemRotationAllowed;
+    }
+
     public boolean isHomeRotationAllowed() {
         return mIsHomeRotationAllowed;
     }
 
-    public int getLauncherRotation() {
-        return mLauncherRotation;
+    public boolean canLauncherRotate() {
+        return isSystemRotationAllowed() && isHomeRotationAllowed();
     }
 
     public int getTouchRotationDegrees() {
@@ -251,6 +281,25 @@
         }
     }
 
+    /**
+     * Returns the scale and pivot so that the provided taskRect can fit the provided full size
+     */
+    public float getFullScreenScaleAndPivot(Rect taskView, DeviceProfile dp, PointF outPivot) {
+        Rect insets = dp.getInsets();
+        float fullWidth = dp.widthPx - insets.left - insets.right;
+        float fullHeight = dp.heightPx - insets.top - insets.bottom;
+        final float scale = LayoutUtils.getTaskScale(this,
+                fullWidth, fullHeight, taskView.width(), taskView.height());
+
+        if (scale == 1) {
+            outPivot.set(fullWidth / 2, fullHeight / 2);
+        } else {
+            float factor = scale / (scale - 1);
+            outPivot.set(taskView.left * factor, taskView.top * factor);
+        }
+        return scale;
+    }
+
     public PagedOrientationHandler getOrientationHandler() {
         return mOrientationHandler;
     }
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 8dbec10..a2c0f23 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -30,7 +30,8 @@
     </style>
 
     <style name="LauncherTheme" parent="@style/BaseLauncherTheme">
-        <item name="allAppsScrimColor">#EAFFFFFF</item>
+        <item name="android:textColorSecondary">#DE000000</item>
+        <item name="allAppsScrimColor">#FFFFFFFF</item>
         <item name="allAppsInterimScrimAlpha">46</item>
         <item name="allAppsNavBarScrimColor">#66FFFFFF</item>
         <item name="popupColorPrimary">#FFF</item>
@@ -89,7 +90,7 @@
         <item name="android:textColorHint">#A0FFFFFF</item>
         <item name="android:colorControlHighlight">#A0FFFFFF</item>
         <item name="android:colorPrimary">#FF212121</item>
-        <item name="allAppsScrimColor">#EA212121</item>
+        <item name="allAppsScrimColor">#FF212121</item>
         <item name="allAppsInterimScrimAlpha">102</item>
         <item name="allAppsNavBarScrimColor">#80000000</item>
         <item name="popupColorPrimary">#3C4043</item> <!-- Gray 800 -->
diff --git a/src/com/android/launcher3/BaseDraggingActivity.java b/src/com/android/launcher3/BaseDraggingActivity.java
index f69c8ed..e4f201c 100644
--- a/src/com/android/launcher3/BaseDraggingActivity.java
+++ b/src/com/android/launcher3/BaseDraggingActivity.java
@@ -181,7 +181,9 @@
                         sourceContainer);
             }
             getUserEventDispatcher().logAppLaunch(v, intent, user);
-            getStatsLogManager().log(APP_LAUNCH_TAP, item.buildProto(null, null));
+
+            getStatsLogManager().log(APP_LAUNCH_TAP, item == null ? null
+                    : item.buildProto(null, null));
             return true;
         } catch (NullPointerException|ActivityNotFoundException|SecurityException e) {
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index bf05a24..c4eab8f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -25,6 +25,8 @@
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.NO_OFFSET;
+import static com.android.launcher3.LauncherState.NO_SCALE;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW_PEEK;
 import static com.android.launcher3.Utilities.postAsyncCallback;
@@ -85,7 +87,6 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.launcher3.DropTarget.DragObject;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
@@ -2698,10 +2699,6 @@
         return new TouchController[] {getDragController(), new AllAppsSwipeController(this)};
     }
 
-    protected ScaleAndTranslation getOverviewScaleAndTranslationForNormalState() {
-        return new ScaleAndTranslation(1.1f, 0f, 0f);
-    }
-
     public void useFadeOutAnimationForLauncherStart(CancellationSignal signal) { }
 
     public void onDragLayerHierarchyChanged() { }
@@ -2724,6 +2721,14 @@
         return Stream.of(APP_INFO, WIDGETS, INSTALL);
     }
 
+
+    /**
+     * @see LauncherState#getOverviewScaleAndOffset(Launcher)
+     */
+    public float[] getNormalOverviewScaleAndOffset() {
+        return new float[] {NO_SCALE, NO_OFFSET};
+    }
+
     public static Launcher getLauncher(Context context) {
         return fromContext(context);
     }
@@ -2735,6 +2740,7 @@
         return (T) activityContext;
     }
 
+
     /**
      * Callback for listening for onResume
      */
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 504666a..54d8f0d 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -90,6 +90,9 @@
     protected static final int FLAG_HIDE_BACK_BUTTON = 1 << 8;
     protected static final int FLAG_HAS_SYS_UI_SCRIM = 1 << 9;
 
+    public static final float NO_OFFSET = 0;
+    public static final float NO_SCALE = 1;
+
     protected static final PageAlphaProvider DEFAULT_ALPHA_PROVIDER =
             new PageAlphaProvider(ACCEL_2) {
                 @Override
@@ -220,7 +223,7 @@
     public abstract int getTransitionDuration(Launcher launcher);
 
     public ScaleAndTranslation getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1, 0, 0);
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public ScaleAndTranslation getHotseatScaleAndTranslation(Launcher launcher) {
@@ -228,12 +231,18 @@
         return getWorkspaceScaleAndTranslation(launcher);
     }
 
-    public ScaleAndTranslation getOverviewScaleAndTranslation(Launcher launcher) {
-        return launcher.getOverviewScaleAndTranslationForNormalState();
+    /**
+     * Returns an array of two elements.
+     *   The first specifies the scale for the overview
+     *   The second is the factor ([0, 1], 0 => center-screen; 1 => offscreen) by which overview
+     *   should be shifted horizontally.
+     */
+    public float[] getOverviewScaleAndOffset(Launcher launcher) {
+        return launcher.getNormalOverviewScaleAndOffset();
     }
 
     public ScaleAndTranslation getQsbScaleAndTranslation(Launcher launcher) {
-        return new ScaleAndTranslation(1, 0, 0);
+        return new ScaleAndTranslation(NO_SCALE, NO_OFFSET, NO_OFFSET);
     }
 
     public float getOverviewFullscreenProgress() {
@@ -276,7 +285,14 @@
      *
      * 0 means completely zoomed in, without blurs. 1 is zoomed out, with blurs.
      */
-    public float getDepth(Context context) {
+    public final float getDepth(Context context) {
+        if (BaseDraggingActivity.fromContext(context).getDeviceProfile().isMultiWindowMode) {
+            return 0;
+        }
+        return getDepthUnchecked(context);
+    }
+
+    protected float getDepthUnchecked(Context context) {
         return 0f;
     }
 
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 2ba624c..b2ff69a 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -78,7 +78,7 @@
     }
 
     @Override
-    public float getDepth(Context context) {
+    protected float getDepthUnchecked(Context context) {
         return 0.5f;
     }
 
diff --git a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
index f9f3bf4..e290685 100644
--- a/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
+++ b/src/com/android/launcher3/touch/LandscapePagedViewHandler.java
@@ -32,7 +32,6 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -120,11 +119,6 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.height();
-    }
-
-    @Override
     public float getPrimarySize(RectF rect) {
         return rect.height();
     }
@@ -135,17 +129,6 @@
     }
 
     @Override
-    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationY = dp.heightPx - view.getPaddingTop();
-        return new ScaleAndTranslation(1f, 0f, offscreenTranslationY);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationY;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_Y;
     }
diff --git a/src/com/android/launcher3/touch/PagedOrientationHandler.java b/src/com/android/launcher3/touch/PagedOrientationHandler.java
index ba4c064..b8396e1 100644
--- a/src/com/android/launcher3/touch/PagedOrientationHandler.java
+++ b/src/com/android/launcher3/touch/PagedOrientationHandler.java
@@ -18,6 +18,7 @@
 
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Matrix;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.RectF;
@@ -28,7 +29,6 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.util.OverScroller;
 
@@ -53,16 +53,15 @@
     Int2DAction<View> VIEW_SCROLL_BY = View::scrollBy;
     Int2DAction<View> VIEW_SCROLL_TO = View::scrollTo;
     Float2DAction<Canvas> CANVAS_TRANSLATE = Canvas::translate;
+    Float2DAction<Matrix> MATRIX_POST_TRANSLATE = Matrix::postTranslate;
+
     <T> void set(T target, Int2DAction<T> action, int param);
     <T> void set(T target, Float2DAction<T> action, float param);
     float getPrimaryDirection(MotionEvent event, int pointerIndex);
     float getPrimaryVelocity(VelocityTracker velocityTracker, int pointerId);
     int getMeasuredSize(View view);
-    int getPrimarySize(Rect rect);
     float getPrimarySize(RectF rect);
     int getSecondaryDimension(View view);
-    LauncherState.ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view);
-    float getTranslationValue(LauncherState.ScaleAndTranslation scaleAndTranslation);
     FloatProperty<View> getPrimaryViewTranslate();
     FloatProperty<View> getSecondaryViewTranslate();
     void setPrimaryAndResetSecondaryTranslate(View view, float translation);
@@ -98,7 +97,6 @@
      */
     void adjustFloatingIconStartVelocity(PointF velocity);
 
-
     class CurveProperties {
         public int scroll;
         public int halfPageSize;
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 7c44eba..dad00a4 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -32,7 +32,6 @@
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherState.ScaleAndTranslation;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.OverScroller;
@@ -118,11 +117,6 @@
     }
 
     @Override
-    public int getPrimarySize(Rect rect) {
-        return rect.width();
-    }
-
-    @Override
     public float getPrimarySize(RectF rect) {
         return rect.width();
     }
@@ -133,17 +127,6 @@
     }
 
     @Override
-    public ScaleAndTranslation getScaleAndTranslation(DeviceProfile dp, View view) {
-        float offscreenTranslationX = dp.widthPx - view.getPaddingStart();
-        return new ScaleAndTranslation(1f, offscreenTranslationX, 0f);
-    }
-
-    @Override
-    public float getTranslationValue(ScaleAndTranslation scaleAndTranslation) {
-        return scaleAndTranslation.translationX;
-    }
-
-    @Override
     public FloatProperty<View> getPrimaryViewTranslate() {
         return VIEW_TRANSLATE_X;
     }
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index 5b3840f..6e21512 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -19,7 +19,6 @@
 import static com.android.launcher3.Utilities.getBadge;
 import static com.android.launcher3.Utilities.getFullDrawable;
 import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
-import static com.android.launcher3.states.RotationHelper.REQUEST_LOCK;
 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
 
 import android.animation.Animator;
@@ -100,6 +99,7 @@
 
     private AnimatorSet mFadeAnimatorSet;
     private ListenerView mListenerView;
+    private Runnable mFastFinishRunnable;
 
     public FloatingIconView(Context context) {
         this(context, null);
@@ -443,9 +443,21 @@
         }
     }
 
+    /**
+     * Sets a runnable that is called after a call to {@link #fastFinish()}.
+     */
+    public void setFastFinishRunnable(Runnable runnable) {
+        mFastFinishRunnable = runnable;
+    }
+
     public void fastFinish() {
+        if (mFastFinishRunnable != null) {
+            mFastFinishRunnable.run();
+            mFastFinishRunnable = null;
+        }
         if (mLoadIconSignal != null) {
             mLoadIconSignal.cancel();
+            mLoadIconSignal = null;
         }
         if (mEndRunnable != null) {
             mEndRunnable.run();
@@ -655,6 +667,7 @@
         sTmpObjArray[0] = null;
         mIconLoadResult = null;
         mClipIconView.recycle();
+        mFastFinishRunnable = null;
     }
 
     private static class IconLoadResult {