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 Original line Diff line number Diff line
@@ -27,6 +27,8 @@ import android.util.Log
import android.util.MathUtils
import android.util.MathUtils
import android.view.View
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup
import android.view.ViewGroupOverlay
import android.view.ViewOverlay
import android.view.animation.Interpolator
import android.view.animation.Interpolator
import android.window.WindowAnimationState
import android.window.WindowAnimationState
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting
@@ -197,10 +199,24 @@ class TransitionAnimator(
    }
    }


    interface Animation {
    interface Animation {
        /** Start the animation. */
        fun start()

        /** Cancel the animation. */
        /** Cancel the animation. */
        fun cancel()
        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. */
    /** The timings (durations and delays) used by this animator. */
    data class Timings(
    data class Timings(
        /** The total duration of the animation. */
        /** The total duration of the animation. */
@@ -270,33 +286,73 @@ class TransitionAnimator(
                alpha = 0
                alpha = 0
            }
            }


        val animator =
        return createAnimation(
            createAnimator(
                controller,
                controller,
                controller.createAnimatorState(),
                endState,
                endState,
                windowBackgroundLayer,
                windowBackgroundLayer,
                fadeWindowBackgroundLayer,
                fadeWindowBackgroundLayer,
                drawHole,
                drawHole,
            )
            )
        animator.start()
            .apply { start() }

        return object : Animation {
            override fun cancel() {
                animator.cancel()
            }
        }
    }
    }


    @VisibleForTesting
    @VisibleForTesting
    fun createAnimator(
    fun createAnimation(
        controller: Controller,
        controller: Controller,
        startState: State,
        endState: State,
        endState: State,
        windowBackgroundLayer: GradientDrawable,
        windowBackgroundLayer: GradientDrawable,
        fadeWindowBackgroundLayer: Boolean = true,
        fadeWindowBackgroundLayer: Boolean = true,
        drawHole: Boolean = false,
        drawHole: Boolean = false,
    ): ValueAnimator {
    ): Animation {
        val state = controller.createAnimatorState()
        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.
        // Start state.
        val startTop = state.top
        val startTop = state.top
        val startBottom = state.bottom
        val startBottom = state.bottom
@@ -333,45 +389,24 @@ class TransitionAnimator(
            }
            }
        }
        }


        val transitionContainer = controller.transitionContainer
        val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
        val isExpandingFullyAbove = isExpandingFullyAbove(transitionContainer, endState)
        var movedBackgroundLayer = false


        // Update state.
        // Update state.
        val animator = ValueAnimator.ofFloat(0f, 1f)
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = timings.totalDuration
        animator.duration = timings.totalDuration
        animator.interpolator = LINEAR
        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(
        animator.addListener(
            object : AnimatorListenerAdapter() {
            object : AnimatorListenerAdapter() {
                override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
                override fun onAnimationStart(animation: Animator, isReverse: Boolean) {
                    if (DEBUG) {
                    onAnimationStart(
                        Log.d(TAG, "Animation started")
                        controller,
                    }
                        isExpandingFullyAbove,
                    controller.onTransitionAnimationStart(isExpandingFullyAbove)
                        windowBackgroundLayer,

                        transitionContainerOverlay,
                    // Add the drawable to the transition container overlay. Overlays always draw
                        openingWindowSyncViewOverlay,
                    // 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)
                    }
                }
                }


                override fun onAnimationEnd(animation: Animator) {
                override fun onAnimationEnd(animation: Animator) {
@@ -413,85 +448,162 @@ class TransitionAnimator(
            state.bottomCornerRadius =
            state.bottomCornerRadius =
                MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)
                MathUtils.lerp(startBottomCornerRadius, endBottomCornerRadius, progress)


            state.visible =
            state.visible = checkVisibility(timings, linearProgress, controller.isLaunching)
                if (controller.isLaunching) {

                    // The expanding view can/should be hidden once it is completely covered by the
            if (!movedBackgroundLayer) {
                    // opening window.
                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(
            getProgress(
                timings,
                timings,
                        linearProgress,
                progress,
                timings.contentBeforeFadeOutDelay,
                timings.contentBeforeFadeOutDelay,
                timings.contentBeforeFadeOutDuration,
                timings.contentBeforeFadeOutDuration,
            ) < 1
            ) < 1
        } else {
        } else {
            // The shrinking view can/should be hidden while it is completely covered by the closing
            // window.
            getProgress(
            getProgress(
                timings,
                timings,
                        linearProgress,
                progress,
                timings.contentAfterFadeInDelay,
                timings.contentAfterFadeInDelay,
                timings.contentAfterFadeInDuration,
                timings.contentAfterFadeInDuration,
            ) > 0
            ) > 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 (
        if (
                controller.isLaunching &&
            controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && !state.visible
                    moveBackgroundLayerWhenAppVisibilityChanges &&
                    !state.visible &&
                    !movedBackgroundLayer
        ) {
        ) {
                // The expanding view is not visible, so the opening app is visible. If this is
            // The expanding view is not visible, so the opening app is visible. If this is the
                // the first frame when it happens, trigger a one-off sync and move the
            // first frame when it happens, trigger a one-off sync and move the background layer
                // background layer in its new container.
            // in its new container.
                movedBackgroundLayer = true

            transitionContainerOverlay.remove(windowBackgroundLayer)
            transitionContainerOverlay.remove(windowBackgroundLayer)
            openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)
            openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)


            ViewRootSync.synchronizeNextDraw(
            ViewRootSync.synchronizeNextDraw(
                transitionContainer,
                transitionContainer,
                    openingWindowSyncView,
                openingWindowSyncView!!,
                then = {},
                then = {},
            )
            )

            return true
        } else if (
        } else if (
                !controller.isLaunching &&
            !controller.isLaunching && moveBackgroundLayerWhenAppVisibilityChanges && state.visible
                    moveBackgroundLayerWhenAppVisibilityChanges &&
                    state.visible &&
                    !movedBackgroundLayer
        ) {
        ) {
                // The contracting view is now visible, so the closing app is not. If this is
            // The contracting view is now visible, so the closing app is not. If this is the first
                // the first frame when it happens, trigger a one-off sync and move the
            // frame when it happens, trigger a one-off sync and move the background layer in its
                // background layer in its new container.
            // new container.
                movedBackgroundLayer = true

            openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
            openingWindowSyncViewOverlay!!.remove(windowBackgroundLayer)
            transitionContainerOverlay.add(windowBackgroundLayer)
            transitionContainerOverlay.add(windowBackgroundLayer)


            ViewRootSync.synchronizeNextDraw(
            ViewRootSync.synchronizeNextDraw(
                    openingWindowSyncView,
                openingWindowSyncView!!,
                transitionContainer,
                transitionContainer,
                then = {},
                then = {},
            )
            )
            }

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


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


        return animator
        return false
    }
    }


    /** Return whether we are expanding fully above the [transitionContainer]. */
    /** Return whether we are expanding fully above the [transitionContainer]. */
+14 −20
Original line number Original line Diff line number Diff line
@@ -52,14 +52,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
        private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"
        private const val GOLDENS_PATH = "frameworks/base/packages/SystemUI/tests/goldens"


        private val emulationSpec =
        private val emulationSpec =
            DeviceEmulationSpec(
            DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160))
                DisplaySpec(
                    "phone",
                    width = 320,
                    height = 690,
                    densityDpi = 160,
                )
            )
    }
    }


    private val kosmos = Kosmos()
    private val kosmos = Kosmos()
@@ -68,7 +61,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
        TransitionAnimator(
        TransitionAnimator(
            kosmos.fakeExecutor,
            kosmos.fakeExecutor,
            ActivityTransitionAnimator.TIMINGS,
            ActivityTransitionAnimator.TIMINGS,
            ActivityTransitionAnimator.INTERPOLATORS
            ActivityTransitionAnimator.INTERPOLATORS,
        )
        )


    @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
    @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(emulationSpec)
@@ -131,16 +124,17 @@ class TransitionAnimatorTest : SysuiTestCase() {
        waitForIdleSync()
        waitForIdleSync()


        val controller = TestController(transitionContainer, isLaunching)
        val controller = TestController(transitionContainer, isLaunching)
        val animator =
        val animation =
            transitionAnimator.createAnimator(
            transitionAnimator.createAnimation(
                controller,
                controller,
                controller.createAnimatorState(),
                createEndState(transitionContainer),
                createEndState(transitionContainer),
                backgroundLayer,
                backgroundLayer,
                fadeWindowBackgroundLayer
                fadeWindowBackgroundLayer,
            )
            ) as TransitionAnimator.InterpolatedAnimation
        return AnimatorSet().apply {
        return AnimatorSet().apply {
            duration = animator.duration
            duration = animation.animator.duration
            play(animator)
            play(animation.animator)
        }
        }
    }
    }


@@ -153,13 +147,13 @@ class TransitionAnimatorTest : SysuiTestCase() {
            right = containerLocation[0] + emulationSpec.display.width,
            right = containerLocation[0] + emulationSpec.display.width,
            bottom = containerLocation[1] + emulationSpec.display.height,
            bottom = containerLocation[1] + emulationSpec.display.height,
            topCornerRadius = 0f,
            topCornerRadius = 0f,
            bottomCornerRadius = 0f
            bottomCornerRadius = 0f,
        )
        )
    }
    }


    private fun recordMotion(
    private fun recordMotion(
        backgroundLayer: GradientDrawable,
        backgroundLayer: GradientDrawable,
        animator: AnimatorSet
        animator: AnimatorSet,
    ): RecordedMotion {
    ): RecordedMotion {
        return motionRule.record(
        return motionRule.record(
            animator,
            animator,
@@ -167,7 +161,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
                feature(DrawableFeatureCaptures.bounds, "bounds")
                feature(DrawableFeatureCaptures.bounds, "bounds")
                feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
                feature(DrawableFeatureCaptures.cornerRadii, "corner_radii")
                feature(DrawableFeatureCaptures.alpha, "alpha")
                feature(DrawableFeatureCaptures.alpha, "alpha")
            }
            },
        )
        )
    }
    }
}
}
@@ -178,7 +172,7 @@ class TransitionAnimatorTest : SysuiTestCase() {
 */
 */
private class TestController(
private class TestController(
    override var transitionContainer: ViewGroup,
    override var transitionContainer: ViewGroup,
    override val isLaunching: Boolean
    override val isLaunching: Boolean,
) : TransitionAnimator.Controller {
) : TransitionAnimator.Controller {
    override fun createAnimatorState(): TransitionAnimator.State {
    override fun createAnimatorState(): TransitionAnimator.State {
        val containerLocation = IntArray(2)
        val containerLocation = IntArray(2)
@@ -189,7 +183,7 @@ private class TestController(
            right = containerLocation[0] + 200,
            right = containerLocation[0] + 200,
            bottom = containerLocation[1] + 400,
            bottom = containerLocation[1] + 400,
            topCornerRadius = 10f,
            topCornerRadius = 10f,
            bottomCornerRadius = 20f
            bottomCornerRadius = 20f,
        )
        )
    }
    }
}
}