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

Commit 16a7bf59 authored by Luca Zuccarini's avatar Luca Zuccarini Committed by Android (Google) Code Review
Browse files

Merge "[1/3] Refactor animator creation to prepare for the new spring." into main

parents 0544b34c a2930f6b
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,
        )
    }
}