Adding TaskViewSimulator for simulating taskView layout on a remote target

1. Tanslating contents of RecentsView instead of recentsView itself. When
   translating, we keep the current TaskView from moving. This allows
   the remoteTarget to follow an individual taskView.
2. Removing translationY in recentsView and setting the pivot instead
3. Using TaskViewSimulator for swipe handler. This allows using similar
   animation creation as Launcher window animation.

Change-Id: I0b7b16c367d9d8cd8dd0ed59061e46853e2f8c83
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..6da804b 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
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..9bb4243 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
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..6a3466c 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)
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/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 18e8768..eee35d1 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.
      */
@@ -352,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 =
@@ -368,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);
@@ -830,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);
@@ -1622,11 +1636,6 @@
     }
 
     @Nullable
-    public TaskView getPreviousTaskView() {
-        return getTaskViewAtByAbsoluteIndex(getRunningTaskIndex() - 1);
-    }
-
-    @Nullable
     public TaskView getCurrentPageTaskView() {
         return getTaskViewAtByAbsoluteIndex(getCurrentPage());
     }
@@ -1685,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() {
@@ -1771,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.
@@ -2038,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/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..33f380f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/AllAppsState.java
@@ -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/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 bc0d2cc..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,6 +48,7 @@
 
 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;
@@ -135,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;
         }
@@ -277,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/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..e799df7 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() {
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;
     }