Merge "[1/3] Refactor animator creation to prepare for the new spring." into main
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index fc4cf1d..3dc0657 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -27,6 +27,8 @@
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewGroupOverlay
+import android.view.ViewOverlay
 import android.view.animation.Interpolator
 import android.window.WindowAnimationState
 import androidx.annotation.VisibleForTesting
@@ -197,10 +199,24 @@
     }
 
     interface Animation {
+        /** Start the animation. */
+        fun start()
+
         /** Cancel the animation. */
         fun cancel()
     }
 
+    @VisibleForTesting
+    class InterpolatedAnimation(@get:VisibleForTesting val animator: Animator) : Animation {
+        override fun start() {
+            animator.start()
+        }
+
+        override fun cancel() {
+            animator.cancel()
+        }
+    }
+
     /** The timings (durations and delays) used by this animator. */
     data class Timings(
         /** The total duration of the animation. */
@@ -270,33 +286,73 @@
                 alpha = 0
             }
 
-        val animator =
-            createAnimator(
+        return createAnimation(
                 controller,
+                controller.createAnimatorState(),
                 endState,
                 windowBackgroundLayer,
                 fadeWindowBackgroundLayer,
                 drawHole,
             )
-        animator.start()
-
-        return object : Animation {
-            override fun cancel() {
-                animator.cancel()
-            }
-        }
+            .apply { start() }
     }
 
     @VisibleForTesting
-    fun createAnimator(
+    fun createAnimation(
         controller: Controller,
+        startState: State,
         endState: State,
         windowBackgroundLayer: GradientDrawable,
         fadeWindowBackgroundLayer: Boolean = true,
         drawHole: Boolean = false,
-    ): ValueAnimator {
-        val state = controller.createAnimatorState()
+    ): Animation {
+        val transitionContainer = controller.transitionContainer
+        val transitionContainerOverlay = transitionContainer.overlay
+        val openingWindowSyncView = controller.openingWindowSyncView
+        val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
 
+        // Whether we should move the [windowBackgroundLayer] into the overlay of
+        // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
+        // from it once the closing app window stops being visible.
+        // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
+        // in complex interactions like launching an activity from a dialog. See
+        // b/214961273#comment2 for more details.
+        val moveBackgroundLayerWhenAppVisibilityChanges =
+            openingWindowSyncView != null &&
+                openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
+
+        return createInterpolatedAnimation(
+            controller,
+            startState,
+            endState,
+            windowBackgroundLayer,
+            transitionContainer,
+            transitionContainerOverlay,
+            openingWindowSyncView,
+            openingWindowSyncViewOverlay,
+            fadeWindowBackgroundLayer,
+            drawHole,
+            moveBackgroundLayerWhenAppVisibilityChanges,
+        )
+    }
+
+    /**
+     * Creates an interpolator-based animator that uses [timings] and [interpolators] to calculate
+     * the new bounds and corner radiuses at each frame.
+     */
+    private fun createInterpolatedAnimation(
+        controller: Controller,
+        state: State,
+        endState: State,
+        windowBackgroundLayer: GradientDrawable,
+        transitionContainer: View,
+        transitionContainerOverlay: ViewGroupOverlay,
+        openingWindowSyncView: View? = null,
+        openingWindowSyncViewOverlay: ViewOverlay? = null,
+        fadeWindowBackgroundLayer: Boolean = true,
+        drawHole: Boolean = false,
+        moveBackgroundLayerWhenAppVisibilityChanges: Boolean = false,
+    ): Animation {
         // Start state.
         val startTop = state.top
         val startBottom = state.bottom
@@ -333,45 +389,24 @@
             }
         }
 
-        val transitionContainer = controller.transitionContainer
         val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
+        var movedBackgroundLayer = false
 
         // Update state.
         val animator = ValueAnimator.ofFloat(0f, 1f)
         animator.duration = timings.totalDuration
         animator.interpolator = LINEAR
 
-        // Whether we should move the [windowBackgroundLayer] into the overlay of
-        // [Controller.openingWindowSyncView] once the opening app window starts to be visible, or
-        // from it once the closing app window stops being visible.
-        // This is necessary as a one-off sync so we can avoid syncing at every frame, especially
-        // in complex interactions like launching an activity from a dialog. See
-        // b/214961273#comment2 for more details.
-        val openingWindowSyncView = controller.openingWindowSyncView
-        val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
-        val moveBackgroundLayerWhenAppVisibilityChanges =
-            openingWindowSyncView != null &&
-                openingWindowSyncView.viewRootImpl != controller.transitionContainer.viewRootImpl
-
-        val transitionContainerOverlay = transitionContainer.overlay
-        var movedBackgroundLayer = false
-
         animator.addListener(
             object : AnimatorListenerAdapter() {
                 override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
-                    if (DEBUG) {
-                        Log.d(TAG, "Animation started")
-                    }
-                    controller.onTransitionAnimationStart(isExpandingFullyAbove)
-
-                    // Add the drawable to the transition container overlay. Overlays always draw
-                    // drawables after views, so we know that it will be drawn above any view added
-                    // by the controller.
-                    if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
-                        transitionContainerOverlay.add(windowBackgroundLayer)
-                    } else {
-                        openingWindowSyncViewOverlay.add(windowBackgroundLayer)
-                    }
+                    onAnimationStart(
+                        controller,
+                        isExpandingFullyAbove,
+                        windowBackgroundLayer,
+                        transitionContainerOverlay,
+                        openingWindowSyncViewOverlay,
+                    )
                 }
 
                 override fun onAnimationEnd(animation: Animator) {
@@ -413,63 +448,20 @@
             state.bottomCornerRadius =
                 MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
 
-            state.visible =
-                if (controller.isLaunching) {
-                    // The expanding view can/should be hidden once it is completely covered by the
-                    // opening window.
-                    getProgress(
-                        timings,
-                        linearProgress,
-                        timings.contentBeforeFadeOutDelay,
-                        timings.contentBeforeFadeOutDuration,
-                    ) < 1
-                } else {
-                    getProgress(
-                        timings,
-                        linearProgress,
-                        timings.contentAfterFadeInDelay,
-                        timings.contentAfterFadeInDuration,
-                    ) > 0
-                }
+            state.visible = checkVisibility(timings, linearProgress, controller.isLaunching)
 
-            if (
-                controller.isLaunching &&
-                    moveBackgroundLayerWhenAppVisibilityChanges &&
-                    !state.visible &&
-                    !movedBackgroundLayer
-            ) {
-                // The expanding view is not visible, so the opening app is visible. If this is
-                // the first frame when it happens, trigger a one-off sync and move the
-                // background layer in its new container.
-                movedBackgroundLayer = true
-
-                transitionContainerOverlay.remove(windowBackgroundLayer)
-                openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
-
-                ViewRootSync.synchronizeNextDraw(
-                    transitionContainer,
-                    openingWindowSyncView,
-                    then = {},
-                )
-            } else if (
-                !controller.isLaunching &&
-                    moveBackgroundLayerWhenAppVisibilityChanges &&
-                    state.visible &&
-                    !movedBackgroundLayer
-            ) {
-                // The contracting view is now visible, so the closing app is not. If this is
-                // the first frame when it happens, trigger a one-off sync and move the
-                // background layer in its new container.
-                movedBackgroundLayer = true
-
-                openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
-                transitionContainerOverlay.add(windowBackgroundLayer)
-
-                ViewRootSync.synchronizeNextDraw(
-                    openingWindowSyncView,
-                    transitionContainer,
-                    then = {},
-                )
+            if (!movedBackgroundLayer) {
+                movedBackgroundLayer =
+                    maybeMoveBackgroundLayer(
+                        controller,
+                        state,
+                        windowBackgroundLayer,
+                        transitionContainer,
+                        transitionContainerOverlay,
+                        openingWindowSyncView,
+                        openingWindowSyncViewOverlay,
+                        moveBackgroundLayerWhenAppVisibilityChanges,
+                    )
             }
 
             val container =
@@ -478,7 +470,6 @@
                 } else {
                     controller.transitionContainer
                 }
-
             applyStateToWindowBackgroundLayer(
                 windowBackgroundLayer,
                 state,
@@ -488,10 +479,131 @@
                 drawHole,
                 controller.isLaunching,
             )
+
             controller.onTransitionAnimationProgress(state, progress, linearProgress)
         }
 
-        return animator
+        return InterpolatedAnimation(animator)
+    }
+
+    private fun onAnimationStart(
+        controller: Controller,
+        isExpandingFullyAbove: Boolean,
+        windowBackgroundLayer: GradientDrawable,
+        transitionContainerOverlay: ViewGroupOverlay,
+        openingWindowSyncViewOverlay: ViewOverlay?,
+    ) {
+        if (DEBUG) {
+            Log.d(TAG, "Animation started")
+        }
+        controller.onTransitionAnimationStart(isExpandingFullyAbove)
+
+        // Add the drawable to the transition container overlay. Overlays always draw
+        // drawables after views, so we know that it will be drawn above any view added
+        // by the controller.
+        if (controller.isLaunching || openingWindowSyncViewOverlay == null) {
+            transitionContainerOverlay.add(windowBackgroundLayer)
+        } else {
+            openingWindowSyncViewOverlay.add(windowBackgroundLayer)
+        }
+    }
+
+    private fun onAnimationEnd(
+        controller: Controller,
+        isExpandingFullyAbove: Boolean,
+        windowBackgroundLayer: GradientDrawable,
+        transitionContainerOverlay: ViewGroupOverlay,
+        openingWindowSyncViewOverlay: ViewOverlay?,
+        moveBackgroundLayerWhenAppVisibilityChanges: Boolean,
+    ) {
+        if (DEBUG) {
+            Log.d(TAG, "Animation ended")
+        }
+
+        // TODO(b/330672236): Post this to the main thread instead so that it does not
+        // flicker with Flexiglass enabled.
+        controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+        transitionContainerOverlay.remove(windowBackgroundLayer)
+
+        if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+            openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+        }
+    }
+
+    /** Returns whether is the controller's view should be visible with the given [timings]. */
+    private fun checkVisibility(timings: Timings, progress: Float, isLaunching: Boolean): Boolean {
+        return if (isLaunching) {
+            // The expanding view can/should be hidden once it is completely covered by the opening
+            // window.
+            getProgress(
+                timings,
+                progress,
+                timings.contentBeforeFadeOutDelay,
+                timings.contentBeforeFadeOutDuration,
+            ) < 1
+        } else {
+            // The shrinking view can/should be hidden while it is completely covered by the closing
+            // window.
+            getProgress(
+                timings,
+                progress,
+                timings.contentAfterFadeInDelay,
+                timings.contentAfterFadeInDuration,
+            ) > 0
+        }
+    }
+
+    /**
+     * If necessary, moves the background layer from the view container's overlay to the window sync
+     * view overlay, or vice versa.
+     *
+     * @return true if the background layer vwas moved, false otherwise.
+     */
+    private fun maybeMoveBackgroundLayer(
+        controller: Controller,
+        state: State,
+        windowBackgroundLayer: GradientDrawable,
+        transitionContainer: View,
+        transitionContainerOverlay: ViewGroupOverlay,
+        openingWindowSyncView: View?,
+        openingWindowSyncViewOverlay: ViewOverlay?,
+        moveBackgroundLayerWhenAppVisibilityChanges: Boolean,
+    ): Boolean {
+        if (
+            controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && !state.visible
+        ) {
+            // The expanding view is not visible, so the opening app is visible. If this is the
+            // first frame when it happens, trigger a one-off sync and move the background layer
+            // in its new container.
+            transitionContainerOverlay.remove(windowBackgroundLayer)
+            openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
+
+            ViewRootSync.synchronizeNextDraw(
+                transitionContainer,
+                openingWindowSyncView!!,
+                then = {},
+            )
+
+            return true
+        } else if (
+            !controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && state.visible
+        ) {
+            // The contracting view is now visible, so the closing app is not. If this is the first
+            // frame when it happens, trigger a one-off sync and move the background layer in its
+            // new container.
+            openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
+            transitionContainerOverlay.add(windowBackgroundLayer)
+
+            ViewRootSync.synchronizeNextDraw(
+                openingWindowSyncView!!,
+                transitionContainer,
+                then = {},
+            )
+
+            return true
+        }
+
+        return false
     }
 
     /** Return whether we are expanding fully above the [transitionContainer]. */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index 6c42662..762cfa0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -52,14 +52,7 @@
         private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
 
         private val emulationSpec =
-            DeviceEmulationSpec(
-                DisplaySpec(
-                    "phone",
-                    width = 320,
-                    height = 690,
-                    densityDpi = 160,
-                )
-            )
+            DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160))
     }
 
     private val kosmos = Kosmos()
@@ -68,7 +61,7 @@
         TransitionAnimator(
             kosmos.fakeExecutor,
             ActivityTransitionAnimator.TIMINGS,
-            ActivityTransitionAnimator.INTERPOLATORS
+            ActivityTransitionAnimator.INTERPOLATORS,
         )
 
     @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@@ -131,16 +124,17 @@
         waitForIdleSync()
 
         val controller = TestController(transitionContainer, isLaunching)
-        val animator =
-            transitionAnimator.createAnimator(
+        val animation =
+            transitionAnimator.createAnimation(
                 controller,
+                controller.createAnimatorState(),
                 createEndState(transitionContainer),
                 backgroundLayer,
-                fadeWindowBackgroundLayer
-            )
+                fadeWindowBackgroundLayer,
+            ) as TransitionAnimator.InterpolatedAnimation
         return AnimatorSet().apply {
-            duration = animator.duration
-            play(animator)
+            duration = animation.animator.duration
+            play(animation.animator)
         }
     }
 
@@ -153,13 +147,13 @@
             right = containerLocation[0] + emulationSpec.display.width,
             bottom = containerLocation[1] + emulationSpec.display.height,
             topCornerRadius = 0f,
-            bottomCornerRadius = 0f
+            bottomCornerRadius = 0f,
         )
     }
 
     private fun recordMotion(
         backgroundLayer: GradientDrawable,
-        animator: AnimatorSet
+        animator: AnimatorSet,
     ): RecordedMotion {
         return motionRule.record(
             animator,
@@ -167,7 +161,7 @@
                 feature(DrawableFeatureCaptures.bounds, "bounds")
                 feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
                 feature(DrawableFeatureCaptures.alpha, "alpha")
-            }
+            },
         )
     }
 }
@@ -178,7 +172,7 @@
  */
 private class TestController(
     override var transitionContainer: ViewGroup,
-    override val isLaunching: Boolean
+    override val isLaunching: Boolean,
 ) : TransitionAnimator.Controller {
     override fun createAnimatorState(): TransitionAnimator.State {
         val containerLocation = IntArray(2)
@@ -189,7 +183,7 @@
             right = containerLocation[0] + 200,
             bottom = containerLocation[1] + 400,
             topCornerRadius = 10f,
-            bottomCornerRadius = 20f
+            bottomCornerRadius = 20f,
         )
     }
 }