Add initial split from GroupedTaskView animation

* Created SplitAnimationController to prevent more
RecentsView bloat
* Extract SplitSelectSource attributes into separate class
* http://recall/-/cMb5xTTxhmZtFt04eYnmQj/eACJq1un8Ef91LgmQblpAw

Bug: 267370106
Change-Id: I5d430c091ac84bbbc30319e7b78711b610a21252
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
new file mode 100644
index 0000000..6dd67de
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -0,0 +1,163 @@
+/*
+ *  Copyright (C) 2023 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 android.animation.ObjectAnimator
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.view.View
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
+import com.android.quickstep.views.TaskThumbnailView
+import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.TaskView.TaskIdAttributeContainer
+import java.util.function.Supplier
+
+/**
+ * Utils class to help run animations for initiating split screen from launcher.
+ * Will be expanded with future refactors. Works in conjunction with the state stored in
+ * [SplitSelectStateController]
+ */
+class SplitAnimationController(val splitSelectStateController: SplitSelectStateController) {
+    companion object {
+        // Break this out into maybe enums? Abstractions into its own classes? Tbd.
+        data class SplitAnimInitProps(
+                val originalView: View,
+                val originalBitmap: Bitmap?,
+                val iconDrawable: Drawable,
+                val fadeWithThumbnail: Boolean,
+                val isStagedTask: Boolean,
+                val iconView: View?
+        )
+    }
+
+    /**
+     * Returns different elements to animate for the initial split selection animation
+     * depending on the state of the surface from which the split was initiated
+     */
+    fun getFirstAnimInitViews(taskViewSupplier: Supplier<TaskView>,
+                              splitSelectSourceSupplier: Supplier<SplitSelectSource>)
+            : SplitAnimInitProps {
+        if (!splitSelectStateController.isAnimateCurrentTaskDismissal) {
+            // Initiating from home
+            val splitSelectSource = splitSelectSourceSupplier.get()
+            return SplitAnimInitProps(splitSelectSource.view, originalBitmap = null,
+                    splitSelectSource.drawable, fadeWithThumbnail = false, isStagedTask = true,
+                    iconView = null)
+        } else if (splitSelectStateController.isDismissingFromSplitPair) {
+            // Initiating split from overview, but on a split pair
+            val taskView = taskViewSupplier.get()
+            for (container : TaskIdAttributeContainer in taskView.taskIdAttributeContainers) {
+                if (container.task.key.id == splitSelectStateController.initialTaskId) {
+                    return SplitAnimInitProps(container.thumbnailView,
+                            container.thumbnailView.thumbnail, container.iconView.drawable!!,
+                            fadeWithThumbnail = true, isStagedTask = true,
+                            iconView = container.iconView
+                    )
+                }
+            }
+            throw IllegalStateException("Attempting to init split from existing split pair " +
+                    "without a valid taskIdAttributeContainer")
+        } else {
+            // Initiating split from overview on fullscreen task TaskView
+            val taskView = taskViewSupplier.get()
+            return SplitAnimInitProps(taskView.thumbnail, taskView.thumbnail.thumbnail,
+                    taskView.iconView.drawable!!, fadeWithThumbnail = true, isStagedTask = true,
+                    taskView.iconView
+            )
+        }
+    }
+
+    /**
+     * When selecting first app from split pair, second app's thumbnail remains. This animates
+     * the second thumbnail by expanding it to take up the full taskViewWidth/Height and overlaying
+     * it with [TaskThumbnailView]'s splashView. Adds animations to the provided builder.
+     * Note: The app that **was not** selected as the first split app should be the container that's
+     * passed through.
+     *
+     * @param builder Adds animation to this
+     * @param taskIdAttributeContainer container of the app that **was not** selected
+     * @param isPrimaryTaskSplitting if true, task that was split would be top/left in the pair
+     *                               (opposite of that representing [taskIdAttributeContainer])
+     */
+    fun addInitialSplitFromPair(taskIdAttributeContainer: TaskIdAttributeContainer,
+                                builder: PendingAnimation, deviceProfile: DeviceProfile,
+                                taskViewWidth: Int, taskViewHeight: Int,
+                                isPrimaryTaskSplitting: Boolean) {
+        val thumbnail = taskIdAttributeContainer.thumbnailView
+        val iconView: View = taskIdAttributeContainer.iconView
+        builder.add(ObjectAnimator.ofFloat(thumbnail, TaskThumbnailView.SPLASH_ALPHA, 1f))
+        thumbnail.setShowSplashForSplitSelection(true)
+        if (deviceProfile.isLandscape) {
+            // Center view first so scaling happens uniformly, alternatively we can move pivotX to 0
+            val centerThumbnailTranslationX: Float = (taskViewWidth - thumbnail.width) / 2f
+            val centerIconTranslationX: Float = (taskViewWidth - iconView.width) / 2f
+            val finalScaleX: Float = taskViewWidth.toFloat() / thumbnail.width
+            builder.add(ObjectAnimator.ofFloat(thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, centerThumbnailTranslationX))
+            // icons are anchored from Gravity.END, so need to use negative translation
+            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
+                    -centerIconTranslationX))
+            builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_X, finalScaleX))
+
+            // Reset other dimensions
+            // TODO(b/271468547), can't set Y translate to 0, need to account for top space
+            thumbnail.scaleY = 1f
+            val translateYResetVal: Float = if (!isPrimaryTaskSplitting) 0f else
+                deviceProfile.overviewTaskThumbnailTopMarginPx.toFloat()
+            builder.add(ObjectAnimator.ofFloat(thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y,
+                    translateYResetVal))
+        } else {
+            val thumbnailSize = taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
+            // Center view first so scaling happens uniformly, alternatively we can move pivotY to 0
+            // primary thumbnail has layout margin above it, so secondary thumbnail needs to take
+            // that into account. We should migrate to only using translations otherwise this
+            // asymmetry causes problems..
+
+            // Icon defaults to center | horizontal, we add additional translation for split
+            val centerIconTranslationX = 0f
+            var centerThumbnailTranslationY: Float
+
+            // TODO(b/271468547), primary thumbnail has layout margin above it, so secondary
+            //  thumbnail needs to take that into account. We should migrate to only using
+            //  translations otherwise this asymmetry causes problems..
+            if (isPrimaryTaskSplitting) {
+                centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
+                centerThumbnailTranslationY += deviceProfile.overviewTaskThumbnailTopMarginPx
+                        .toFloat()
+            } else {
+                centerThumbnailTranslationY = (thumbnailSize - thumbnail.height) / 2f
+            }
+            val finalScaleY: Float = thumbnailSize.toFloat() / thumbnail.height
+            builder.add(ObjectAnimator.ofFloat(thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_Y, centerThumbnailTranslationY))
+
+            // icons are anchored from Gravity.END, so need to use negative translation
+            builder.add(ObjectAnimator.ofFloat(iconView, View.TRANSLATION_X,
+                    centerIconTranslationX))
+            builder.add(ObjectAnimator.ofFloat(thumbnail, View.SCALE_Y, finalScaleY))
+
+            // Reset other dimensions
+            thumbnail.scaleX = 1f
+            builder.add(ObjectAnimator.ofFloat(thumbnail,
+                    TaskThumbnailView.SPLIT_SELECT_TRANSLATE_X, 0f))
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 5214f7c..1f4085f 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -82,6 +82,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final RecentsModel mRecentTasksModel;
+    private final SplitAnimationController mSplitAnimationController;
     private StatsLogManager mStatsLogManager;
     private final SystemUiProxy mSystemUiProxy;
     private final StateManager mStateManager;
@@ -96,6 +97,11 @@
     private boolean mRecentsAnimationRunning;
     /** If {@code true}, animates the existing task view split placeholder view */
     private boolean mAnimateCurrentTaskDismissal;
+    /**
+     * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a
+     * split pair task view without wanting to animate current task dismissal overall
+     */
+    private boolean mDismissingFromSplitPair;
     @Nullable
     private UserHandle mUser;
     /** If not null, this is the TaskView we want to launch from */
@@ -116,6 +122,7 @@
         mStateManager = stateManager;
         mDepthController = depthController;
         mRecentTasksModel = recentsModel;
+        mSplitAnimationController = new SplitAnimationController(this);
     }
 
     /**
@@ -399,6 +406,18 @@
         mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal;
     }
 
+    public boolean isDismissingFromSplitPair() {
+        return mDismissingFromSplitPair;
+    }
+
+    public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) {
+        mDismissingFromSplitPair = dismissingFromSplitPair;
+    }
+
+    public SplitAnimationController getSplitAnimationController() {
+        return mSplitAnimationController;
+    }
+
     /**
      * Requires Shell Transitions
      */
@@ -506,6 +525,7 @@
         mItemInfo = null;
         mSplitEvent = null;
         mAnimateCurrentTaskDismissal = false;
+        mDismissingFromSplitPair = false;
     }
 
     /**
@@ -532,6 +552,10 @@
         return mInitialTaskId;
     }
 
+    public int getSecondTaskId() {
+        return mSecondTaskId;
+    }
+
     private boolean isSecondTaskIntentSet() {
         return (mSecondTaskId != INVALID_TASK_ID || mSecondTaskIntent != null);
     }
diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
index 14898b1..cc05779 100644
--- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java
@@ -500,7 +500,7 @@
     }
 
     @Override
-    void setThumbnailVisibility(int visibility) {
+    void setThumbnailVisibility(int visibility, int taskId) {
         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
             mSnapshotViewMap.valueAt(i).setVisibility(visibility);
         }
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index e9498fd..0e05032 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -1,5 +1,7 @@
 package com.android.quickstep.views;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
 
@@ -25,6 +27,7 @@
 import com.android.quickstep.TaskThumbnailCache;
 import com.android.quickstep.util.CancellableTask;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -267,6 +270,19 @@
 
     @Override
     protected int getLastSelectedChildTaskIndex() {
+        SplitSelectStateController splitSelectController =
+                getRecentsView().getSplitSelectController();
+        if (splitSelectController.isDismissingFromSplitPair()) {
+            // return the container index of the task that wasn't initially selected to split with
+            // because that is the only remaining app that can be selected. The coordinate checks
+            // below aren't reliable since both of those views may be gone/transformed
+            int initSplitTaskId = getThisTaskCurrentlyInSplitSelection();
+            if (initSplitTaskId != INVALID_TASK_ID) {
+                return initSplitTaskId == mTask.key.id ? 1 : 0;
+            }
+        }
+
+        // Check which of the two apps was selected
         if (isCoordInView(mIconView2, mLastTouchDownPosition)
                 || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) {
             return 1;
@@ -296,9 +312,30 @@
         if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) {
             return;
         }
-        getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView,
-                mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig,
-                mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+        int initSplitTaskId = getThisTaskCurrentlyInSplitSelection();
+        if (initSplitTaskId == INVALID_TASK_ID) {
+            getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView,
+                    mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig,
+                    mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+            // Should we be having a separate translation step apart from the measuring above?
+            // The following only applies to large screen for now, but for future reference
+            // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary
+            // translation directions
+            mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX());
+            mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY());
+            mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX());
+            mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY());
+        } else {
+            // Currently being split with this taskView, let the non-split selected thumbnail
+            // take up full thumbnail area
+            TaskIdAttributeContainer container =
+                    mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0];
+            container.getThumbnailView().measure(widthMeasureSpec,
+                    View.MeasureSpec.makeMeasureSpec(
+                            heightSize -
+                                    mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx,
+                            MeasureSpec.EXACTLY));
+        }
         updateIconPlacement();
     }
 
@@ -379,21 +416,27 @@
         mSnapshotView2.refreshSplashView();
     }
 
+    @Override
+    protected void resetViewTransforms() {
+        super.resetViewTransforms();
+        mSnapshotView2.resetViewTransforms();
+    }
+
     /**
-     *     Sets visibility for thumbnails and associated elements (DWB banners).
-     *     IconView is unaffected.
+     * Sets visibility for thumbnails and associated elements (DWB banners).
+     * IconView is unaffected.
      *
-     *     When setting INVISIBLE, sets the visibility for the last selected child task.
-     *     When setting VISIBLE (as a reset), sets the visibility for both tasks.
+     * When setting INVISIBLE, sets the visibility for the last selected child task.
+     * When setting VISIBLE (as a reset), sets the visibility for both tasks.
      */
     @Override
-    void setThumbnailVisibility(int visibility) {
+    void setThumbnailVisibility(int visibility, int taskId) {
         if (visibility == VISIBLE) {
             mSnapshotView.setVisibility(visibility);
             mDigitalWellBeingToast.setBannerVisibility(visibility);
             mSnapshotView2.setVisibility(visibility);
             mDigitalWellBeingToast2.setBannerVisibility(visibility);
-        } else if (getLastSelectedChildTaskIndex() == 0) {
+        } else if (taskId == getTaskIds()[0]) {
             mSnapshotView.setVisibility(visibility);
             mDigitalWellBeingToast.setBannerVisibility(visibility);
         } else {
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index ac59403..076fd64 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -186,6 +186,7 @@
 import com.android.quickstep.util.GroupTask;
 import com.android.quickstep.util.LayoutUtils;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.quickstep.util.SplitAnimationController.Companion.SplitAnimInitProps;
 import com.android.quickstep.util.SplitAnimationTimings;
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.SurfaceTransaction;
@@ -194,6 +195,7 @@
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.quickstep.util.TransformParams;
 import com.android.quickstep.util.VibrationConstants;
+import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
 import com.android.systemui.plugins.ResourceProvider;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -937,7 +939,7 @@
         if (mHandleTaskStackChanges) {
             TaskView taskView = getTaskViewByTaskId(taskId);
             if (taskView != null) {
-                for (TaskView.TaskIdAttributeContainer container :
+                for (TaskIdAttributeContainer container :
                         taskView.getTaskIdAttributeContainers()) {
                     if (container == null || taskId != container.getTask().key.id) {
                         continue;
@@ -3096,29 +3098,26 @@
 
         RectF startingTaskRect = new RectF();
         safeRemoveDragLayerView(mFirstFloatingTaskView);
+        SplitAnimInitProps splitAnimInitProps =
+                mSplitSelectStateController.getSplitAnimationController().getFirstAnimInitViews(
+                        () -> mSplitHiddenTaskView, () -> mSplitSelectSource);
         if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) {
             // Create the split select animation from Overview
-            mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE);
-            anim.setViewAlpha(mSplitHiddenTaskView.getIconView(), 0, clampToProgress(LINEAR,
+            mSplitHiddenTaskView.setThumbnailVisibility(INVISIBLE,
+                    mSplitSelectStateController.getInitialTaskId());
+            anim.setViewAlpha(splitAnimInitProps.getIconView(), 0, clampToProgress(LINEAR,
                     timings.getIconFadeStartOffset(),
                     timings.getIconFadeEndOffset()));
-            mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
-                    mSplitHiddenTaskView.getThumbnail(),
-                    mSplitHiddenTaskView.getThumbnail().getThumbnail(),
-                    mSplitHiddenTaskView.getIconView().getDrawable(), startingTaskRect);
-            mFirstFloatingTaskView.setAlpha(1);
-            mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
-                    true /* fadeWithThumbnail */, true /* isStagedTask */);
-        } else {
-            // Create the split select animation from Home
-            mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
-                    mSplitSelectSource.view, null /* thumbnail */,
-                    mSplitSelectSource.drawable, startingTaskRect);
-            mFirstFloatingTaskView.setAlpha(1);
-            mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
-                    false /* fadeWithThumbnail */, true /* isStagedTask */);
         }
 
+        mFirstFloatingTaskView = FloatingTaskView.getFloatingTaskView(mActivity,
+                splitAnimInitProps.getOriginalView(),
+                splitAnimInitProps.getOriginalBitmap(),
+                splitAnimInitProps.getIconDrawable(), startingTaskRect);
+        mFirstFloatingTaskView.setAlpha(1);
+        mFirstFloatingTaskView.addStagingAnimation(anim, startingTaskRect, mTempRect,
+                splitAnimInitProps.getFadeWithThumbnail(), splitAnimInitProps.isStagedTask());
+
         // Allow user to click staged app to launch into fullscreen
         if (ENABLE_LAUNCH_FROM_STAGED_APP.get()) {
             mFirstFloatingTaskView.setOnClickListener(this::animateToFullscreen);
@@ -4447,7 +4446,7 @@
         mTaskViewsSecondarySplitTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView taskView = requireTaskViewAt(i);
-            if (taskView == mSplitHiddenTaskView) {
+            if (taskView == mSplitHiddenTaskView && !taskView.containsMultipleTasks()) {
                 continue;
             }
             taskView.getSecondarySplitTranslationProperty().set(taskView, translation);
@@ -4499,6 +4498,10 @@
         mSplitHiddenTaskViewIndex = indexOfChild(mSplitHiddenTaskView);
         mSplitSelectStateController
                 .setAnimateCurrentTaskDismissal(splitSelectSource.animateCurrentTaskDismissal);
+
+        // Prevent dismissing whole task if we're only initiating from one of 2 tasks in split pair
+        mSplitSelectStateController.setDismissingFromSplitPair(mSplitHiddenTaskView != null
+                && mSplitHiddenTaskView.containsMultipleTasks());
         mSplitSelectStateController.setInitialTaskSelect(splitSelectSource.intent,
                 splitSelectSource.position.stagePosition, splitSelectSource.itemInfo,
                 splitSelectSource.splitEvent, splitSelectSource.alreadyRunningTaskId);
@@ -4517,8 +4520,32 @@
      * Modifies a PendingAnimation with the animations for entering split staging
      */
     public void createSplitSelectInitAnimation(PendingAnimation builder, int duration) {
-        if (mSplitSelectStateController.isAnimateCurrentTaskDismissal()) {
-            // Splitting from Overview
+        boolean isInitiatingSplitFromTaskView =
+                mSplitSelectStateController.isAnimateCurrentTaskDismissal();
+        boolean isInitiatingTaskViewSplitPair =
+                mSplitSelectStateController.isDismissingFromSplitPair();
+        if (isInitiatingSplitFromTaskView && isInitiatingTaskViewSplitPair) {
+            // Splitting from Overview for split pair task
+            createInitialSplitSelectAnimation(builder);
+
+            // Animate pair thumbnail into full thumbnail
+            boolean primaryTaskSelected =
+                    mSplitHiddenTaskView.getTaskIdAttributeContainers()[0].getTask().key.id ==
+                            mSplitSelectStateController.getInitialTaskId();
+            TaskIdAttributeContainer taskIdAttributeContainer = mSplitHiddenTaskView
+                    .getTaskIdAttributeContainers()[primaryTaskSelected ? 1 : 0];
+            TaskThumbnailView thumbnail = taskIdAttributeContainer.getThumbnailView();
+            mSplitSelectStateController.getSplitAnimationController()
+                    .addInitialSplitFromPair(taskIdAttributeContainer, builder,
+                            mActivity.getDeviceProfile(),
+                            mSplitHiddenTaskView.getWidth(), mSplitHiddenTaskView.getHeight(),
+                            primaryTaskSelected);
+            builder.addOnFrameCallback(() ->{
+                thumbnail.refreshSplashView();
+                mSplitHiddenTaskView.updateSnapshotRadius();
+            });
+        } else if (isInitiatingSplitFromTaskView) {
+            // Splitting from Overview for fullscreen task
             createTaskDismissAnimation(builder, mSplitHiddenTaskView, true, false, duration,
                     true /* dismissingForSplitSelection*/);
         } else {
@@ -4606,7 +4633,8 @@
 
         mSecondSplitHiddenView = containerTaskView;
         if (mSecondSplitHiddenView != null) {
-            mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE);
+            mSecondSplitHiddenView.setThumbnailVisibility(INVISIBLE,
+                    mSplitSelectStateController.getSecondTaskId());
         }
 
         InteractionJankMonitorWrapper.begin(this,
@@ -4632,7 +4660,7 @@
         }
 
         if (mSecondSplitHiddenView != null) {
-            mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE);
+            mSecondSplitHiddenView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
             mSecondSplitHiddenView = null;
         }
 
@@ -4658,7 +4686,7 @@
         resetTaskVisuals();
         mSplitHiddenTaskViewIndex = -1;
         if (mSplitHiddenTaskView != null) {
-            mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE);
+            mSplitHiddenTaskView.setThumbnailVisibility(VISIBLE, INVALID_TASK_ID);
             mSplitHiddenTaskView = null;
         }
         if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) {
@@ -5528,7 +5556,7 @@
         }
 
         taskView.setShowScreenshot(true);
-        for (TaskView.TaskIdAttributeContainer container :
+        for (TaskIdAttributeContainer container :
                 taskView.getTaskIdAttributeContainers()) {
             if (container == null) {
                 continue;
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 432eadc..62a58f5 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -81,6 +81,47 @@
                 }
             };
 
+    public static final Property<TaskThumbnailView, Float> SPLASH_ALPHA =
+            new FloatProperty<TaskThumbnailView>("splashAlpha") {
+                @Override
+                public void setValue(TaskThumbnailView thumbnail, float splashAlpha) {
+                    thumbnail.setSplashAlpha(splashAlpha);
+                }
+
+                @Override
+                public Float get(TaskThumbnailView thumbnailView) {
+                    return thumbnailView.mSplashAlpha / 255f;
+                }
+            };
+
+    /** Use to animate thumbnail translationX while first app in split selection is initiated */
+    public static final Property<TaskThumbnailView, Float> SPLIT_SELECT_TRANSLATE_X =
+            new FloatProperty<TaskThumbnailView>("splitSelectTranslateX") {
+                @Override
+                public void setValue(TaskThumbnailView thumbnail, float splitSelectTranslateX) {
+                    thumbnail.applySplitSelectTranslateX(splitSelectTranslateX);
+                }
+
+                @Override
+                public Float get(TaskThumbnailView thumbnailView) {
+                    return thumbnailView.mSplitSelectTranslateX;
+                }
+            };
+
+    /** Use to animate thumbnail translationY while first app in split selection is initiated */
+    public static final Property<TaskThumbnailView, Float> SPLIT_SELECT_TRANSLATE_Y =
+            new FloatProperty<TaskThumbnailView>("splitSelectTranslateY") {
+                @Override
+                public void setValue(TaskThumbnailView thumbnail, float splitSelectTranslateY) {
+                    thumbnail.applySplitSelectTranslateY(splitSelectTranslateY);
+                }
+
+                @Override
+                public Float get(TaskThumbnailView thumbnailView) {
+                    return thumbnailView.mSplitSelectTranslateY;
+                }
+            };
+
     private final BaseActivity mActivity;
     @Nullable
     private TaskOverlay mOverlay;
@@ -111,6 +152,10 @@
     private int mSplashAlpha = 0;
 
     private boolean mOverlayEnabled;
+    /** Used as a placeholder when the original thumbnail animates out to. */
+    private boolean mShowSplashForSplitSelection;
+    private float mSplitSelectTranslateX;
+    private float mSplitSelectTranslateY;
 
     public TaskThumbnailView(Context context) {
         this(context, null);
@@ -342,10 +387,17 @@
 
         // Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios.
         if (shouldShowSplashView()) {
+            float cornerRadiusX = cornerRadius;
+            float cornerRadiusY = cornerRadius;
+            if (mShowSplashForSplitSelection) {
+                cornerRadiusX = cornerRadius / getScaleX();
+                cornerRadiusY = cornerRadius / getScaleY();
+            }
+
             // Always draw background for hiding inconsistencies, even if splash view is not yet
             // loaded (which can happen as task icons are loaded asynchronously in the background)
-            canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius,
-                    cornerRadius, mSplashBackgroundPaint);
+            canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadiusX,
+                    cornerRadiusY, mSplashBackgroundPaint);
             if (mSplashView != null) {
                 mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1);
                 mSplashView.draw(canvas);
@@ -353,6 +405,31 @@
         }
     }
 
+    /** See {@link #SPLIT_SELECT_TRANSLATE_X} */
+    protected void applySplitSelectTranslateX(float splitSelectTranslateX) {
+        mSplitSelectTranslateX = splitSelectTranslateX;
+        applyTranslateX();
+    }
+
+    /** See {@link #SPLIT_SELECT_TRANSLATE_Y} */
+    protected void applySplitSelectTranslateY(float splitSelectTranslateY) {
+        mSplitSelectTranslateY = splitSelectTranslateY;
+        applyTranslateY();
+    }
+
+    private void applyTranslateX() {
+        setTranslationX(mSplitSelectTranslateX);
+    }
+
+    private void applyTranslateY() {
+        setTranslationY(mSplitSelectTranslateY);
+    }
+
+    protected void resetViewTransforms() {
+        mSplitSelectTranslateX = 0;
+        mSplitSelectTranslateY = 0;
+    }
+
     public TaskView getTaskView() {
         return (TaskView) getParent();
     }
@@ -373,7 +450,12 @@
      */
     public boolean shouldShowSplashView() {
         return isThumbnailAspectRatioDifferentFromThumbnailData()
-                || isThumbnailRotationDifferentFromTask();
+                || isThumbnailRotationDifferentFromTask()
+                || mShowSplashForSplitSelection;
+    }
+
+    public void setShowSplashForSplitSelection(boolean showSplashForSplitSelection) {
+        mShowSplashForSplitSelection = showSplashForSplitSelection;
     }
 
     protected void refreshSplashView() {
@@ -396,7 +478,6 @@
 
         imageView.setScaleType(ImageView.ScaleType.MATRIX);
         Matrix matrix = new Matrix();
-
         float drawableWidth = mSplashViewDrawable.getIntrinsicWidth();
         float drawableHeight = mSplashViewDrawable.getIntrinsicHeight();
         float viewWidth = getMeasuredWidth();
@@ -408,12 +489,13 @@
         float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale();
         float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null
                 ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen();
-        float scale = nonGridScale * recentsMaxScale;
+        float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX());
+        float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY());
 
         // Center the image in the view.
         matrix.setTranslate(centeredDrawableLeft, centeredDrawableTop);
         // Apply scale transformation after translation, pivoting around center of view.
-        matrix.postScale(scale, scale, viewCenterX, viewCenterY);
+        matrix.postScale(scaleX, scaleY, viewCenterX, viewCenterY);
 
         imageView.setImageMatrix(matrix);
         mSplashView = imageView;
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index ab72f2d..a06e1f8 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -16,6 +16,7 @@
 
 package com.android.quickstep.views;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.widget.Toast.LENGTH_SHORT;
 
@@ -41,6 +42,7 @@
 import android.animation.ObjectAnimator;
 import android.annotation.IdRes;
 import android.app.ActivityOptions;
+import android.app.ActivityTaskManager;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Canvas;
@@ -706,9 +708,12 @@
         }
         SplitSelectStateController splitSelectStateController =
                 recentsView.getSplitSelectController();
-        if (splitSelectStateController.isSplitSelectActive() &&
-                splitSelectStateController.getInitialTaskId() == getTask().key.id) {
-            // Prevent taps on the this taskview if it's being animated into split select state
+        // Disable taps for split selection animation unless we have multiple tasks
+        boolean disableTapsForSplitSelect =
+                splitSelectStateController.isSplitSelectActive()
+                        && splitSelectStateController.getInitialTaskId() == getTask().key.id
+                        && !containsMultipleTasks();
+        if (disableTapsForSplitSelect) {
             return false;
         }
 
@@ -718,6 +723,25 @@
         return super.dispatchTouchEvent(ev);
     }
 
+    /**
+     * @return taskId that split selection was initiated with,
+     *         {@link ActivityTaskManager#INVALID_TASK_ID} if no tasks in this TaskView are part of
+     *         split selection
+     */
+    protected int getThisTaskCurrentlyInSplitSelection() {
+        SplitSelectStateController splitSelectController =
+                getRecentsView().getSplitSelectController();
+        int initSplitTaskId = INVALID_TASK_ID;
+        for (TaskIdAttributeContainer container : getTaskIdAttributeContainers()) {
+            int taskId = container.getTask().key.id;
+            if (taskId == splitSelectController.getInitialTaskId()) {
+                initSplitTaskId = taskId;
+                break;
+            }
+        }
+        return initSplitTaskId;
+    }
+
     private void onClick(View view) {
         if (getTask() == null) {
             return;
@@ -747,6 +771,8 @@
 
     /**
      * Returns the task index of the last selected child task (0 or 1).
+     * If we contain multiple tasks and this TaskView is used as part of split selection, the
+     * selected child task index will be that of the remaining task.
      */
     protected int getLastSelectedChildTaskIndex() {
         return 0;
@@ -1084,6 +1110,8 @@
         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
         int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
 
+        // TODO(b/271468547), we should default to setting trasnlations only on the snapshot instead
+        //  of a hybrid of both margins and translations
         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
         snapshotParams.topMargin = thumbnailTopMargin;
         mSnapshotView.setLayoutParams(snapshotParams);
@@ -1179,6 +1207,7 @@
         setAlpha(mStableAlpha);
         setIconScaleAndDim(1);
         setColorTint(0, 0);
+        mSnapshotView.resetViewTransforms();
     }
 
     public void setStableAlpha(float parentAlpha) {
@@ -1720,10 +1749,12 @@
     }
 
     /**
-     *     Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
-     *     IconView is unaffected.
+     *  Sets visibility for the thumbnail and associated elements (DWB banners and action chips).
+     *  IconView is unaffected.
+     *
+     * @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value
      */
-    void setThumbnailVisibility(int visibility) {
+    void setThumbnailVisibility(int visibility, int taskId) {
         for (int i = 0; i < getChildCount(); i++) {
             View child = getChildAt(i);
             if (child != mIconView) {
diff --git a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
index 628aa9a..295baa3 100644
--- a/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
+++ b/src/com/android/launcher3/touch/PortraitPagedViewHandler.java
@@ -635,6 +635,9 @@
                 primarySnapshot.setTranslationX(0);
             }
             secondarySnapshot.setTranslationY(spaceAboveSnapshot);
+
+            // Reset unused translations
+            primarySnapshot.setTranslationY(0);
         } else {
             int deviceHeightWithoutTaskbar = dp.availableHeightPx - dp.taskbarSize;
             float scale = (float) totalThumbnailHeight / deviceHeightWithoutTaskbar;
@@ -669,6 +672,10 @@
                 View.MeasureSpec.makeMeasureSpec(secondarySnapshotWidth, View.MeasureSpec.EXACTLY),
                 View.MeasureSpec.makeMeasureSpec(secondarySnapshotHeight,
                         View.MeasureSpec.EXACTLY));
+        primarySnapshot.setScaleX(1);
+        secondarySnapshot.setScaleX(1);
+        primarySnapshot.setScaleY(1);
+        secondarySnapshot.setScaleY(1);
     }
 
     @Override
@@ -699,13 +706,13 @@
                     : deviceProfile.getInsets().left;
             int fullscreenMidpointFromBottom = ((deviceProfile.widthPx
                     - fullscreenInsetThickness) / 2);
-            float midpointFromBottomPct = (float) fullscreenMidpointFromBottom
+            float midpointFromEndPct = (float) fullscreenMidpointFromBottom
                     / deviceProfile.widthPx;
             float insetPct = (float) fullscreenInsetThickness / deviceProfile.widthPx;
             int spaceAboveSnapshots = 0;
             int overviewThumbnailAreaThickness = groupedTaskViewWidth - spaceAboveSnapshots;
             int bottomToMidpointOffset = (int) (overviewThumbnailAreaThickness
-                    * midpointFromBottomPct);
+                    * midpointFromEndPct);
             int insetOffset = (int) (overviewThumbnailAreaThickness * insetPct);
 
             if (deviceProfile.isSeascape()) {