Implementing an unfold animation using CannedAnimationController

Bug: 297057373
Flag: aconfig enable_unfold_state_animation DISABLED
Test: Manual
Change-Id: Iaa352133ebf06ab74b9ba4b8c4d3523f27666089
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index f34105a..5703711 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -127,3 +127,10 @@
     description: "Enables asnc inflation of workspace icons"
     bug: "318539160"
 }
+
+flag {
+    name: "enable_unfold_state_animation"
+    namespace: "launcher"
+    description: "Tie unfold animation with state animation"
+    bug: "297057373"
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 33641a4..7c7c426 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -22,6 +22,7 @@
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
 
 import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
 import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
@@ -405,8 +406,12 @@
      */
     private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity(
             StatefulActivity activity) {
-        if (activity instanceof QuickstepLauncher) {
-            return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider();
+        if (!enableUnfoldStateAnimation()) {
+            if (activity instanceof QuickstepLauncher ql) {
+                return ql.getUnfoldTransitionProgressProvider();
+            }
+        } else {
+            return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider();
         }
         return null;
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index f0ab08c..b1ea0de 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -21,6 +21,7 @@
 import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
 
 import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
 import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
 import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
@@ -170,6 +171,8 @@
 import com.android.quickstep.util.SplitToWorkspaceController;
 import com.android.quickstep.util.SplitWithKeyboardShortcutController;
 import com.android.quickstep.util.TISBindHelper;
+import com.android.quickstep.util.unfold.LauncherUnfoldTransitionController;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.quickstep.views.FloatingTaskView;
 import com.android.quickstep.views.OverviewActionsView;
 import com.android.quickstep.views.RecentsView;
@@ -955,9 +958,17 @@
     }
 
     private void initUnfoldTransitionProgressProvider() {
-        final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
-        if (config.isEnabled()) {
-            initRemotelyCalculatedUnfoldAnimation(config);
+        if (!enableUnfoldStateAnimation()) {
+            final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
+            if (config.isEnabled()) {
+                initRemotelyCalculatedUnfoldAnimation(config);
+            }
+        } else {
+            ProxyUnfoldTransitionProvider provider =
+                    SystemUiProxy.INSTANCE.get(this).getUnfoldTransitionProvider();
+            if (provider != null) {
+                new LauncherUnfoldTransitionController(this, provider);
+            }
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 723af43..56a4024 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,6 +17,7 @@
 
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
 
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 import static com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
@@ -67,6 +68,7 @@
 import com.android.launcher3.util.Preconditions;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -74,6 +76,7 @@
 import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
 import com.android.systemui.shared.system.smartspace.SmartspaceState;
+import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig;
 import com.android.systemui.unfold.progress.IUnfoldAnimation;
 import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
 import com.android.wm.shell.back.IBackAnimation;
@@ -177,7 +180,10 @@
      */
     private final PendingIntent mRecentsPendingIntent;
 
-    public SystemUiProxy(Context context) {
+    @Nullable
+    private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
+
+    private SystemUiProxy(Context context) {
         mContext = context;
         mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
         final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -187,6 +193,10 @@
         mRecentsPendingIntent = PendingIntent.getActivity(mContext, 0, baseIntent,
                 PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
                         | Intent.FILL_IN_COMPONENT, options.toBundle());
+
+        mUnfoldTransitionProvider =
+                (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled())
+                         ? new ProxyUnfoldTransitionProvider() : null;
     }
 
     @Override
@@ -251,7 +261,7 @@
         mRecentTasks = recentTasks;
         mBackAnimation = backAnimation;
         mDesktopMode = desktopMode;
-        mUnfoldAnimation = unfoldAnimation;
+        mUnfoldAnimation = enableUnfoldStateAnimation() ? null : unfoldAnimation;
         mDragAndDrop = dragAndDrop;
         linkToDeath();
         // re-attach the listeners once missing due to setProxy has not been initialized yet.
@@ -272,6 +282,19 @@
         setAssistantOverridesRequested(
                 AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
         mStateChangeCallbacks.forEach(Runnable::run);
+
+        if (mUnfoldTransitionProvider != null) {
+            if (unfoldAnimation != null) {
+                try {
+                    unfoldAnimation.setListener(mUnfoldTransitionProvider);
+                    mUnfoldTransitionProvider.setActive(true);
+                } catch (RemoteException e) {
+                    // Ignore
+                }
+            } else {
+                mUnfoldTransitionProvider.setActive(false);
+            }
+        }
     }
 
     /**
@@ -1451,6 +1474,11 @@
         }
     }
 
+    @Nullable
+    public ProxyUnfoldTransitionProvider getUnfoldTransitionProvider() {
+        return mUnfoldTransitionProvider;
+    }
+
     //
     // Recents
     //
diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
new file mode 100644
index 0000000..54d317d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.unfold
+
+import android.app.Activity
+import android.os.Trace
+import android.view.Surface
+import com.android.launcher3.Alarm
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
+import com.android.launcher3.Launcher
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+/** Controls animations that are happening during unfolding foldable devices */
+class LauncherUnfoldTransitionController(
+    private val launcher: Launcher,
+    private val progressProvider: ProxyUnfoldTransitionProvider
+) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
+
+    private var isTablet: Boolean? = null
+    private var hasUnfoldTransitionStarted = false
+    private val timeoutAlarm =
+        Alarm().apply {
+            setOnAlarmListener {
+                onTransitionFinished()
+                Trace.endAsyncSection("$TAG#startedPreemptively", 0)
+            }
+        }
+
+    init {
+        launcher.addOnDeviceProfileChangeListener(this)
+        launcher.registerActivityLifecycleCallbacks(this)
+    }
+
+    override fun onActivityPaused(activity: Activity) {
+        progressProvider.removeCallback(this)
+    }
+
+    override fun onActivityResumed(activity: Activity) {
+        progressProvider.addCallback(this)
+    }
+
+    override fun onDeviceProfileChanged(dp: DeviceProfile) {
+        if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
+            return
+        }
+
+        if (isTablet != null && dp.isTablet != isTablet) {
+            // We should preemptively start the animation only if:
+            // - We changed to the unfolded screen
+            // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't
+            //   receive transition progress events from SystemUI later because there was no
+            //   IPC connection established (e.g. because of SystemUI crash)
+            // - SystemUI has not already sent unfold animation progress events. This might happen
+            //   if Launcher was not open during unfold, in this case we receive the configuration
+            //   change only after we went back to home screen and we don't want to start the
+            //   animation in this case.
+            if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) {
+                // Preemptively start the unfold animation to make sure that we have drawn
+                // the first frame of the animation before the screen gets unblocked
+                onTransitionStarted()
+                Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
+                timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS)
+            }
+            if (!dp.isTablet) {
+                // Reset unfold transition status when folded
+                hasUnfoldTransitionStarted = false
+            }
+        }
+
+        isTablet = dp.isTablet
+    }
+
+    override fun onTransitionStarted() {
+        hasUnfoldTransitionStarted = true
+        launcher.animationCoordinator.setAnimation(
+            provider = this,
+            factory = this::onPrepareUnfoldAnimation,
+            duration =
+                1000L // The expected duration for the animation. Then only comes to play if we have
+            // to run the animation ourselves in case sysui misses the end signal
+        )
+        timeoutAlarm.cancelAlarm()
+    }
+
+    override fun onTransitionProgress(progress: Float) {
+        hasUnfoldTransitionStarted = true
+        launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress)
+    }
+
+    override fun onTransitionFinished() {
+        // Run the animation to end the animation in case it is not already at end progress. It
+        // will scale the duration to the remaining progress
+        launcher.animationCoordinator.getPlaybackController(this)?.start()
+        timeoutAlarm.cancelAlarm()
+    }
+
+    private fun onPrepareUnfoldAnimation(anim: PendingAnimation) {
+        val dp = launcher.deviceProfile
+        val rotation = dp.displayInfo.rotation
+        val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+        UnfoldAnimationBuilder.buildUnfoldAnimation(
+            launcher,
+            isVertical,
+            dp.displayInfo.currentSize,
+            anim
+        )
+    }
+
+    companion object {
+        private const val TAG = "LauncherUnfoldTransitionController"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
new file mode 100644
index 0000000..83c7f72
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.unfold
+
+import androidx.annotation.AnyThread
+import androidx.annotation.FloatRange
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.progress.IUnfoldTransitionListener
+import com.android.systemui.unfold.progress.UnfoldRemoteFilter
+
+/** Receives unfold events from remote senders (System UI). */
+class ProxyUnfoldTransitionProvider :
+    UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
+
+    private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
+    private val delegate = UnfoldRemoteFilter(ProcessedProgressListener())
+
+    private var transitionStarted = false
+    var isActive = false
+        set(value) {
+            field = value
+            if (!value) {
+                // Finish any active transition
+                onTransitionFinished()
+            }
+        }
+
+    @AnyThread
+    override fun onTransitionStarted() {
+        MAIN_EXECUTOR.execute(delegate::onTransitionStarted)
+    }
+
+    @AnyThread
+    override fun onTransitionProgress(progress: Float) {
+        MAIN_EXECUTOR.execute { delegate.onTransitionProgress(progress) }
+    }
+
+    @AnyThread
+    override fun onTransitionFinished() {
+        MAIN_EXECUTOR.execute(delegate::onTransitionFinished)
+    }
+
+    override fun addCallback(listener: TransitionProgressListener) {
+        listeners += listener
+        if (transitionStarted) {
+            // Update the listener in case there was is an active transition
+            listener.onTransitionStarted()
+        }
+    }
+
+    override fun removeCallback(listener: TransitionProgressListener) {
+        listeners -= listener
+        if (transitionStarted) {
+            // Finish the transition if it was already running
+            listener.onTransitionFinished()
+        }
+    }
+
+    override fun destroy() {
+        listeners.clear()
+    }
+
+    private inner class ProcessedProgressListener : TransitionProgressListener {
+        override fun onTransitionStarted() {
+            if (!transitionStarted) {
+                transitionStarted = true
+                listeners.forEach(TransitionProgressListener::onTransitionStarted)
+            }
+        }
+
+        override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+            listeners.forEach { it.onTransitionProgress(progress) }
+        }
+
+        override fun onTransitionFinished() {
+            if (transitionStarted) {
+                transitionStarted = false
+                listeners.forEach(TransitionProgressListener::onTransitionFinished)
+            }
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
new file mode 100644
index 0000000..d2c4728
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.unfold
+
+import android.graphics.Point
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.animation.Interpolators.clampToProgress
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.Workspace
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.util.HorizontalInsettableView
+
+private typealias ViewGroupAction = (ViewGroup, Boolean) -> Unit
+
+object UnfoldAnimationBuilder {
+
+    private val CLIP_CHILDREN: ViewGroupAction = ViewGroup::setClipChildren
+    private val CLIP_TO_PADDING: ViewGroupAction = ViewGroup::setClipToPadding
+
+    data class RestoreInfo(val action: ViewGroupAction, var target: ViewGroup, var value: Boolean)
+
+    // Percentage of the width of the quick search bar that will be reduced
+    // from the both sides of the bar when progress is 0
+    private const val MAX_WIDTH_INSET_FRACTION = 0.04f
+
+    // Scale factor for the whole workspace and hotseat
+    private const val SCALE_LAUNCHER_FROM = 0.92f
+
+    // Translation factor for all the items on the homescreen
+    private const val TRANSLATION_PERCENTAGE = 0.08f
+
+    private fun setClipChildren(
+        target: ViewGroup,
+        value: Boolean,
+        restoreList: MutableList<RestoreInfo>
+    ) {
+        val originalValue = target.clipChildren
+        if (originalValue != value) {
+            target.clipChildren = value
+            restoreList.add(RestoreInfo(CLIP_CHILDREN, target, originalValue))
+        }
+    }
+
+    private fun setClipToPadding(
+        target: ViewGroup,
+        value: Boolean,
+        restoreList: MutableList<RestoreInfo>
+    ) {
+        val originalValue = target.clipToPadding
+        if (originalValue != value) {
+            target.clipToPadding = value
+            restoreList.add(RestoreInfo(CLIP_TO_PADDING, target, originalValue))
+        }
+    }
+
+    private fun addChildrenAnimation(
+        itemsContainer: ViewGroup,
+        isVerticalFold: Boolean,
+        screenSize: Point,
+        anim: PendingAnimation
+    ) {
+        val tempLocation = IntArray(2)
+        for (i in 0 until itemsContainer.childCount) {
+            val child = itemsContainer.getChildAt(i)
+
+            child.getLocationOnScreen(tempLocation)
+            if (isVerticalFold) {
+                val viewCenterX = tempLocation[0] + child.width / 2
+                val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
+                anim.addFloat(
+                    child,
+                    VIEW_TRANSLATE_X,
+                    distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+                    0f,
+                    LINEAR
+                )
+            } else {
+                val viewCenterY = tempLocation[1] + child.height / 2
+                val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
+                anim.addFloat(
+                    child,
+                    VIEW_TRANSLATE_Y,
+                    distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+                    0f,
+                    LINEAR
+                )
+            }
+        }
+    }
+
+    /**
+     * Builds an animation for the unfold experience and adds it to the provided PendingAnimation
+     */
+    fun buildUnfoldAnimation(
+        launcher: Launcher,
+        isVerticalFold: Boolean,
+        screenSize: Point,
+        anim: PendingAnimation
+    ) {
+        val restoreList = ArrayList<RestoreInfo>()
+        val registerViews: (CellLayout) -> Unit = { cellLayout ->
+            setClipChildren(cellLayout, false, restoreList)
+            setClipToPadding(cellLayout, false, restoreList)
+            addChildrenAnimation(cellLayout.shortcutsAndWidgets, isVerticalFold, screenSize, anim)
+        }
+
+        val workspace: Workspace<*> = launcher.workspace
+        val hotseat = launcher.hotseat
+
+        // Animation icons from workspace for all orientations
+        workspace.forEachVisiblePage { registerViews(it as CellLayout) }
+        setClipChildren(workspace, false, restoreList)
+        setClipToPadding(workspace, true, restoreList)
+
+        // Workspace scale
+        launcher.workspace.setPivotToScaleWithSelf(launcher.hotseat)
+        val interpolator = clampToProgress(LINEAR, 0f, 1f)
+        anim.addFloat(
+            workspace,
+            WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+            SCALE_LAUNCHER_FROM,
+            1f,
+            interpolator
+        )
+        anim.addFloat(
+            hotseat,
+            HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+            SCALE_LAUNCHER_FROM,
+            1f,
+            interpolator
+        )
+
+        if (isVerticalFold) {
+            if (hotseat.qsb is HorizontalInsettableView) {
+                anim.addFloat(
+                    hotseat.qsb as HorizontalInsettableView,
+                    HorizontalInsettableView.HORIZONTAL_INSETS,
+                    MAX_WIDTH_INSET_FRACTION,
+                    0f,
+                    LINEAR
+                )
+            }
+            registerViews(hotseat)
+        }
+        anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } }
+    }
+}