Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a2930f6b authored by Luca Zuccarini's avatar Luca Zuccarini
Browse files

[1/3] Refactor animator creation to prepare for the new spring.

For return animations, in some scenarios we will be taking over
transitions in which the outgoing window has momentum (e.g. gesture nav,
predictive back). In these cases, using the default interpolators can
cause a jarring stagger between the tracking motion and the non-tracking
portion. Instead we will use a spring which can keep the momentum while
maintaining a similar duration and curve as the interpolators.

Bug: 323863002
Flag: EXEMPT refactor only
Test: atest TransitionAnimatorTest
Change-Id: I29ce76fd6a739144ff4945d3c9a6da0b1a0fb1db
parent 61cc9d30
Loading
Loading
Loading
Loading
+212 −100
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.util.Log
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 @@ class TransitionAnimator(
    }

    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 @@ class TransitionAnimator(
                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 @@ class TransitionAnimator(
            }
        }

        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,85 +448,162 @@ class TransitionAnimator(
            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.
            state.visible = checkVisibility(timings, linearProgress, controller.isLaunching)

            if (!movedBackgroundLayer) {
                movedBackgroundLayer =
                    maybeMoveBackgroundLayer(
                        controller,
                        state,
                        windowBackgroundLayer,
                        transitionContainer,
                        transitionContainerOverlay,
                        openingWindowSyncView,
                        openingWindowSyncViewOverlay,
                        moveBackgroundLayerWhenAppVisibilityChanges,
                    )
            }

            val container =
                if (movedBackgroundLayer) {
                    openingWindowSyncView!!
                } else {
                    controller.transitionContainer
                }
            applyStateToWindowBackgroundLayer(
                windowBackgroundLayer,
                state,
                linearProgress,
                container,
                fadeWindowBackgroundLayer,
                drawHole,
                controller.isLaunching,
            )

            controller.onTransitionAnimationProgress(state, progress, linearProgress)
        }

        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,
                        linearProgress,
                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,
                        linearProgress,
                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 &&
                    !movedBackgroundLayer
            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.
                movedBackgroundLayer = true

            // 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,
                openingWindowSyncView!!,
                then = {},
            )

            return true
        } else if (
                !controller.isLaunching &&
                    moveBackgroundLayerWhenAppVisibilityChanges &&
                    state.visible &&
                    !movedBackgroundLayer
            !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.
                movedBackgroundLayer = true

            // 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,
                openingWindowSyncView!!,
                transitionContainer,
                then = {},
            )
            }

            val container =
                if (movedBackgroundLayer) {
                    openingWindowSyncView!!
                } else {
                    controller.transitionContainer
                }

            applyStateToWindowBackgroundLayer(
                windowBackgroundLayer,
                state,
                linearProgress,
                container,
                fadeWindowBackgroundLayer,
                drawHole,
                controller.isLaunching,
            )
            controller.onTransitionAnimationProgress(state, progress, linearProgress)
            return true
        }

        return animator
        return false
    }

    /** Return whether we are expanding fully above the [transitionContainer]. */
+14 −20
Original line number Diff line number Diff line
@@ -52,14 +52,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
        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 @@ class TransitionAnimatorTest : SysuiTestCase() {
        TransitionAnimator(
            kosmos.fakeExecutor,
            ActivityTransitionAnimator.TIMINGS,
            ActivityTransitionAnimator.INTERPOLATORS
            ActivityTransitionAnimator.INTERPOLATORS,
        )

    @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@@ -131,16 +124,17 @@ class TransitionAnimatorTest : SysuiTestCase() {
        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 @@ class TransitionAnimatorTest : SysuiTestCase() {
            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 @@ class TransitionAnimatorTest : SysuiTestCase() {
                feature(DrawableFeatureCaptures.bounds, "bounds")
                feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
                feature(DrawableFeatureCaptures.alpha, "alpha")
            }
            },
        )
    }
}
@@ -178,7 +172,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
 */
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 @@ private class TestController(
            right = containerLocation[0] + 200,
            bottom = containerLocation[1] + 400,
            topCornerRadius = 10f,
            bottomCornerRadius = 20f
            bottomCornerRadius = 20f,
        )
    }
}