diff options
| author | 2024-10-09 13:41:12 +0000 | |
|---|---|---|
| committer | 2024-10-09 13:41:12 +0000 | |
| commit | 16a7bf59161d7cdf9ae03fd55dbea29bc7e9eefd (patch) | |
| tree | 303f830d6793896f1e2158d3204a23908c714639 | |
| parent | 0544b34c0d5d7b7368163b2bbb9f5123bedffad5 (diff) | |
| parent | a2930f6bbecc8f85c3cbc1805e16231ccc76ec98 (diff) | |
Merge "[1/3] Refactor animator creation to prepare for the new spring." into main
| -rw-r--r-- | packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt | 312 | ||||
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt | 34 |
2 files changed, 226 insertions, 120 deletions
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 fc4cf1d1e21e..3dc0657f0d0d 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.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,63 +448,20 @@ 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. - getProgress( - timings, - linearProgress, - timings.contentBeforeFadeOutDelay, - timings.contentBeforeFadeOutDuration, - ) < 1 - } else { - getProgress( - timings, - linearProgress, - timings.contentAfterFadeInDelay, - timings.contentAfterFadeInDuration, - ) > 0 - } - - 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 = {}, - ) + state.visible = checkVisibility(timings, linearProgress, controller.isLaunching) + + if (!movedBackgroundLayer) { + movedBackgroundLayer = + maybeMoveBackgroundLayer( + controller, + state, + windowBackgroundLayer, + transitionContainer, + transitionContainerOverlay, + openingWindowSyncView, + openingWindowSyncViewOverlay, + moveBackgroundLayerWhenAppVisibilityChanges, + ) } val container = @@ -478,7 +470,6 @@ class TransitionAnimator( } else { controller.transitionContainer } - applyStateToWindowBackgroundLayer( windowBackgroundLayer, state, @@ -488,10 +479,131 @@ class TransitionAnimator( 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 6c42662f2cdd..762cfa03c310 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 @@ 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, ) } } |