From 9ba35a2704454ad63340deb53839addb38f62551 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 10 Nov 2021 12:50:05 +0100 Subject: [PATCH 1/3] Synchronize dialog launch animation using BLAST This CL synchronizes the start of the launch and the end of the exit dialog animations using BLAST. This replaces the previous synchronization that was performed using preDraw listeners, which was not optimal as it led to flickering (and was also slower). Bug: 193634619 Test: Manual Change-Id: I30bd56fcd082b76c4c7693da8c1d59bc2f6f781b --- .../animation/DialogLaunchAnimator.kt | 150 ++++++++++-------- .../animation/DialogLaunchAnimatorTest.kt | 3 +- 2 files changed, 84 insertions(+), 69 deletions(-) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index de82ebdc6b1c4..5e7e8c5313ca4 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -28,10 +28,11 @@ import android.service.dreams.IDreamManager import android.util.Log import android.util.MathUtils import android.view.GhostView +import android.view.SurfaceControl import android.view.View import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewTreeObserver.OnPreDrawListener +import android.view.ViewRootImpl import android.view.WindowManager import android.widget.FrameLayout import kotlin.math.roundToInt @@ -42,10 +43,11 @@ private const val TAG = "DialogLaunchAnimator" * A class that allows dialogs to be started in a seamless way from a view that is transforming * nicely into the starting dialog. */ -class DialogLaunchAnimator( +class DialogLaunchAnimator @JvmOverloads constructor( private val context: Context, private val launchAnimator: LaunchAnimator, - private val dreamManager: IDreamManager + private val dreamManager: IDreamManager, + private var isForTesting: Boolean = false ) { private companion object { private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running @@ -103,7 +105,8 @@ class DialogLaunchAnimator( onDialogDismissed = { openedDialogs.remove(it) }, dialog = dialog, animateBackgroundBoundsChange, - animatedParent + animatedParent, + isForTesting ) openedDialogs.add(animatedDialog) @@ -177,7 +180,13 @@ private class AnimatedDialog( private val animateBackgroundBoundsChange: Boolean, /** Launch animation corresponding to the parent [AnimatedDialog]. */ - private val parentAnimatedDialog: AnimatedDialog? = null + private val parentAnimatedDialog: AnimatedDialog? = null, + + /** + * Whether we are currently running in a test, in which case we need to disable + * synchronization. + */ + private val isForTesting: Boolean ) { /** * The DecorView of this dialog window. @@ -365,59 +374,77 @@ private class AnimatedDialog( // Show the dialog. dialog.show() - // Add a temporary touch surface ghost as soon as the window is ready to draw. This - // temporary ghost will be drawn together with the touch surface, but in the dialog - // window. Once it is drawn, we will make the touch surface invisible, and then start the - // animation. We do all this synchronization to avoid flicker that would occur if we made - // the touch surface invisible too early (before its ghost is drawn), leading to one or more - // frames with a hole instead of the touch surface (or its ghost). - decorView.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { - override fun onPreDraw(): Boolean { - decorView.viewTreeObserver.removeOnPreDrawListener(this) - addTemporaryTouchSurfaceGhost() - return true - } - }) - decorView.invalidate() + addTouchSurfaceGhost() } - private fun addTemporaryTouchSurfaceGhost() { + private fun addTouchSurfaceGhost() { + if (decorView.viewRootImpl == null) { + // Make sure that we have access to the dialog view root to synchronize the creation of + // the ghost. + decorView.post(::addTouchSurfaceGhost) + return + } + // Create a ghost of the touch surface (which will make the touch surface invisible) and add - // it to the dialog. We will wait for this ghost to be drawn before starting the animation. - val ghost = GhostView.addGhost(touchSurface, decorView) - - // The ghost of the touch surface was just created, so the touch surface was made invisible. - // We make it visible again until the ghost is actually drawn. - touchSurface.visibility = View.VISIBLE - - // Wait for the ghost to be drawn before continuing. - ghost.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { - override fun onPreDraw(): Boolean { - ghost.viewTreeObserver.removeOnPreDrawListener(this) - onTouchSurfaceGhostDrawn() - return true - } + // it to the host dialog. We trigger a one off synchronization to make sure that this is + // done in sync between the two different windows. + synchronizeNextDraw(then = { + isTouchSurfaceGhostDrawn = true + maybeStartLaunchAnimation() }) - ghost.invalidate() + GhostView.addGhost(touchSurface, decorView) + + // The ghost of the touch surface was just created, so the touch surface is currently + // invisible. We need to make sure that it stays invisible as long as the dialog is shown or + // animating. + (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true) } - private fun onTouchSurfaceGhostDrawn() { - // Make the touch surface invisible and make sure that it stays invisible as long as the - // dialog is shown or animating. - touchSurface.visibility = View.INVISIBLE - (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true) + /** + * Synchronize the next draw of the touch surface and dialog view roots so that they are + * performed at the same time, in the same transaction. This is necessary to make sure that the + * ghost of the touch surface is drawn at the same time as the touch surface is made invisible + * (or inversely, removed from the UI when the touch surface is made visible). + */ + private fun synchronizeNextDraw(then: () -> Unit) { + if (isForTesting || !touchSurface.isAttachedToWindow || touchSurface.viewRootImpl == null || + !decorView.isAttachedToWindow || decorView.viewRootImpl == null) { + // No need to synchronize if either the touch surface or dialog view is not attached + // to a window. + then() + return + } - // Add a pre draw listener to (maybe) start the animation once the touch surface is - // actually invisible. - touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { - override fun onPreDraw(): Boolean { - touchSurface.viewTreeObserver.removeOnPreDrawListener(this) - isTouchSurfaceGhostDrawn = true - maybeStartLaunchAnimation() - return true + // Consume the next frames of both view roots to make sure the ghost view is drawn at + // exactly the same time as when the touch surface is made invisible. + var remainingTransactions = 0 + val mergedTransactions = SurfaceControl.Transaction() + + fun onTransaction(transaction: SurfaceControl.Transaction?) { + remainingTransactions-- + transaction?.let { mergedTransactions.merge(it) } + + if (remainingTransactions == 0) { + mergedTransactions.apply() + then() } - }) - touchSurface.invalidate() + } + + fun consumeNextDraw(viewRootImpl: ViewRootImpl) { + if (viewRootImpl.consumeNextDraw(::onTransaction)) { + remainingTransactions++ + + // Make sure we trigger a traversal. + viewRootImpl.view.invalidate() + } + } + + consumeNextDraw(touchSurface.viewRootImpl) + consumeNextDraw(decorView.viewRootImpl) + + if (remainingTransactions == 0) { + then() + } } private fun findFirstViewGroupWithBackground(view: View): ViewGroup? { @@ -556,25 +583,12 @@ private class AnimatedDialog( .removeOnLayoutChangeListener(backgroundLayoutListener) } - // The animated ghost was just removed. We create a temporary ghost that will be - // removed only once we draw the touch surface, to avoid flickering that would - // happen when removing the ghost too early (before the touch surface is drawn). - GhostView.addGhost(touchSurface, decorView) - - touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { - override fun onPreDraw(): Boolean { - touchSurface.viewTreeObserver.removeOnPreDrawListener(this) - - // Now that the touch surface was drawn, we can remove the temporary ghost - // and instantly dismiss the dialog. - GhostView.removeGhost(touchSurface) - onAnimationFinished(true /* instantDismiss */) - onDialogDismissed(this@AnimatedDialog) - - return true - } + // Make sure that the removal of the ghost and making the touch surface visible is + // done at the same time. + synchronizeNextDraw(then = { + onAnimationFinished(true /* instantDismiss */) + onDialogDismissed(this@AnimatedDialog) }) - touchSurface.invalidate() } ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index f9ad740f86df9..ee1784d919d58 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -42,7 +42,8 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { @Before fun setUp() { - dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager) + dialogLaunchAnimator = DialogLaunchAnimator( + context, launchAnimator, dreamManager, isForTesting = true) } @After From 401bc29face36af7cc9efe5ffb84d3c2006cb2ae Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Mon, 29 Nov 2021 15:48:06 +0100 Subject: [PATCH 2/3] Extract interpolators and durations out of LaunchAnimator This CL extracts the durations and interpolators from LaunchAnimator so that we can use different durations and interpolators in the DialogLaunchAnimator (compared to the ActivityLaunchAnimator). This CL is a no-op and does not change the interpolators and durations yet. Those will be changed in a follow-up CL. Bug: 208241926 Test: Manual Change-Id: Ic4d7def45cabc8c73700efcec67706eaf81b2b9e --- .../launch_animation_interpolator_x.xml | 18 --- .../launch_animation_interpolator_y.xml | 18 --- .../animation/ActivityLaunchAnimator.kt | 50 +++++++-- .../animation/DialogLaunchAnimator.kt | 16 ++- .../systemui/animation/LaunchAnimator.kt | 106 +++++++++++++----- .../dagger/StatusBarDependenciesModule.java | 18 +-- .../notification/ExpandAnimationParameters.kt | 4 +- .../NotificationPanelViewController.java | 8 +- .../StatusBarLaunchAnimatorController.kt | 3 +- .../animation/ActivityLaunchAnimatorTest.kt | 2 +- .../animation/DialogLaunchAnimatorTest.kt | 4 +- .../android/systemui/animation/TestValues.kt | 23 ++++ 12 files changed, 164 insertions(+), 106 deletions(-) delete mode 100644 packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml delete mode 100644 packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml create mode 100644 packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml deleted file mode 100644 index 620dd4891b9bf..0000000000000 --- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_x.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml b/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml deleted file mode 100644 index a268abce0c27e..0000000000000 --- a/packages/SystemUI/animation/res/interpolator/launch_animation_interpolator_y.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt index 7020603383591..a61b18c4cb281 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityLaunchAnimator.kt @@ -21,6 +21,7 @@ import android.app.ActivityTaskManager import android.app.PendingIntent import android.app.TaskInfo import android.graphics.Matrix +import android.graphics.Path import android.graphics.Rect import android.graphics.RectF import android.os.Looper @@ -34,6 +35,7 @@ import android.view.SyncRtSurfaceTransactionApplier import android.view.View import android.view.ViewGroup import android.view.WindowManager +import android.view.animation.Interpolator import android.view.animation.PathInterpolator import com.android.internal.annotations.VisibleForTesting import com.android.internal.policy.ScreenDecorationsUtils @@ -45,16 +47,46 @@ private const val TAG = "ActivityLaunchAnimator" * A class that allows activities to be started in a seamless way from a view that is transforming * nicely into the starting window. */ -class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { +class ActivityLaunchAnimator( + private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS) +) { companion object { + @JvmField + val TIMINGS = LaunchAnimator.Timings( + totalDuration = 500L, + contentBeforeFadeOutDelay = 0L, + contentBeforeFadeOutDuration = 150L, + contentAfterFadeInDelay = 150L, + contentAfterFadeInDuration = 183L + ) + + val INTERPOLATORS = LaunchAnimator.Interpolators( + positionInterpolator = Interpolators.EMPHASIZED, + positionXInterpolator = createPositionXInterpolator(), + contentBeforeFadeOutInterpolator = Interpolators.LINEAR_OUT_SLOW_IN, + contentAfterFadeInInterpolator = PathInterpolator(0f, 0f, 0.6f, 1f) + ) + + /** Durations & interpolators for the navigation bar fading in & out. */ private const val ANIMATION_DURATION_NAV_FADE_IN = 266L private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L - private const val ANIMATION_DELAY_NAV_FADE_IN = - LaunchAnimator.ANIMATION_DURATION - ANIMATION_DURATION_NAV_FADE_IN + private val ANIMATION_DELAY_NAV_FADE_IN = + TIMINGS.totalDuration - ANIMATION_DURATION_NAV_FADE_IN + + private val NAV_FADE_IN_INTERPOLATOR = Interpolators.STANDARD_DECELERATE + private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) + + /** The time we wait before timing out the remote animation after starting the intent. */ private const val LAUNCH_TIMEOUT = 1000L - private val NAV_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0f, 1f) - private val NAV_FADE_OUT_INTERPOLATOR = PathInterpolator(0.2f, 0f, 1f, 1f) + private fun createPositionXInterpolator(): Interpolator { + val path = Path().apply { + moveTo(0f, 0f) + cubicTo(0.1217f, 0.0462f, 0.15f, 0.4686f, 0.1667f, 0.66f) + cubicTo(0.1834f, 0.8878f, 0.1667f, 1f, 1f, 1f) + } + return PathInterpolator(path) + } } /** @@ -107,8 +139,8 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { val animationAdapter = if (!hideKeyguardWithAnimation) { RemoteAnimationAdapter( runner, - LaunchAnimator.ANIMATION_DURATION, - LaunchAnimator.ANIMATION_DURATION - 150 /* statusBarTransitionDelay */ + TIMINGS.totalDuration, + TIMINGS.totalDuration - 150 /* statusBarTransitionDelay */ ) } else { null @@ -449,7 +481,7 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { state: LaunchAnimator.State, linearProgress: Float ) { - val fadeInProgress = LaunchAnimator.getProgress(linearProgress, + val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT) val params = SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(navigationBar.leash) @@ -464,7 +496,7 @@ class ActivityLaunchAnimator(private val launchAnimator: LaunchAnimator) { .withWindowCrop(windowCrop) .withVisibility(true) } else { - val fadeOutProgress = LaunchAnimator.getProgress(linearProgress, 0, + val fadeOutProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress, 0, ANIMATION_DURATION_NAV_FADE_OUT) params.withAlpha(1f - NAV_FADE_OUT_INTERPOLATOR.getInterpolation(fadeOutProgress)) } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 5e7e8c5313ca4..1a6c66f383027 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -20,7 +20,6 @@ import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.app.Dialog -import android.content.Context import android.graphics.Color import android.graphics.Rect import android.os.Looper @@ -44,12 +43,13 @@ private const val TAG = "DialogLaunchAnimator" * nicely into the starting dialog. */ class DialogLaunchAnimator @JvmOverloads constructor( - private val context: Context, - private val launchAnimator: LaunchAnimator, private val dreamManager: IDreamManager, + private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS), private var isForTesting: Boolean = false ) { private companion object { + private val TIMINGS = ActivityLaunchAnimator.TIMINGS + private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running } @@ -98,7 +98,6 @@ class DialogLaunchAnimator @JvmOverloads constructor( animateFrom.setTag(TAG_LAUNCH_ANIMATION_RUNNING, true) val animatedDialog = AnimatedDialog( - context, launchAnimator, dreamManager, animateFrom, @@ -160,7 +159,6 @@ class DialogLaunchAnimator @JvmOverloads constructor( } private class AnimatedDialog( - private val context: Context, private val launchAnimator: LaunchAnimator, private val dreamManager: IDreamManager, @@ -177,7 +175,7 @@ private class AnimatedDialog( val dialog: Dialog, /** Whether we should animate the dialog background when its bounds change. */ - private val animateBackgroundBoundsChange: Boolean, + animateBackgroundBoundsChange: Boolean, /** Launch animation corresponding to the parent [AnimatedDialog]. */ private val parentAnimatedDialog: AnimatedDialog? = null, @@ -275,14 +273,14 @@ private class AnimatedDialog( // and the view that we added so that we can dismiss the dialog when this view is // clicked. This is necessary because DecorView overrides onTouchEvent and therefore we // can't set the click listener directly on the (now fullscreen) DecorView. - val fullscreenTransparentBackground = FrameLayout(context) + val fullscreenTransparentBackground = FrameLayout(dialog.context) decorView.addView( fullscreenTransparentBackground, 0 /* index */, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) ) - val dialogContentWithBackground = FrameLayout(context) + val dialogContentWithBackground = FrameLayout(dialog.context) dialogContentWithBackground.background = decorView.background // Make the window background transparent. Note that setting the window (or DecorView) @@ -510,7 +508,7 @@ private class AnimatedDialog( private fun onDialogDismissed() { if (Looper.myLooper() != Looper.getMainLooper()) { - context.mainExecutor.execute { onDialogDismissed() } + dialog.context.mainExecutor.execute { onDialogDismissed() } return } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt index 3bf6c5ebd091a..fbd09c03afd7e 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt @@ -27,25 +27,19 @@ import android.util.Log import android.util.MathUtils import android.view.View import android.view.ViewGroup -import android.view.animation.AnimationUtils -import android.view.animation.PathInterpolator +import android.view.animation.Interpolator +import com.android.systemui.animation.Interpolators.LINEAR import kotlin.math.roundToInt private const val TAG = "LaunchAnimator" /** A base class to animate a window launch (activity or dialog) from a view . */ -class LaunchAnimator @JvmOverloads constructor( - context: Context, - private val isForTesting: Boolean = false +class LaunchAnimator( + private val timings: Timings, + private val interpolators: Interpolators ) { companion object { internal const val DEBUG = false - const val ANIMATION_DURATION = 500L - private const val ANIMATION_DURATION_FADE_OUT_CONTENT = 150L - private const val ANIMATION_DURATION_FADE_IN_WINDOW = 183L - private const val ANIMATION_DELAY_FADE_IN_WINDOW = ANIMATION_DURATION_FADE_OUT_CONTENT - - private val WINDOW_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.6f, 1f) private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC) /** @@ -53,23 +47,20 @@ class LaunchAnimator @JvmOverloads constructor( * sub-animation starting [delay] ms after the launch animation and that lasts [duration]. */ @JvmStatic - fun getProgress(linearProgress: Float, delay: Long, duration: Long): Float { + fun getProgress( + timings: Timings, + linearProgress: Float, + delay: Long, + duration: Long + ): Float { return MathUtils.constrain( - (linearProgress * ANIMATION_DURATION - delay) / duration, + (linearProgress * timings.totalDuration - delay) / duration, 0.0f, 1.0f ) } } - /** The interpolator used for the width, height, Y position and corner radius. */ - private val animationInterpolator = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_y) - - /** The interpolator used for the X position. */ - private val animationInterpolatorX = AnimationUtils.loadInterpolator(context, - R.interpolator.launch_animation_interpolator_x) - private val launchContainerLocation = IntArray(2) private val cornerRadii = FloatArray(8) @@ -159,6 +150,45 @@ class LaunchAnimator @JvmOverloads constructor( fun cancel() } + /** The timings (durations and delays) used by this animator. */ + class Timings( + /** The total duration of the animation. */ + val totalDuration: Long, + + /** The time to wait before fading out the expanding content. */ + val contentBeforeFadeOutDelay: Long, + + /** The duration of the expanding content fade out. */ + val contentBeforeFadeOutDuration: Long, + + /** + * The time to wait before fading in the expanded content (usually an activity or dialog + * window). + */ + val contentAfterFadeInDelay: Long, + + /** The duration of the expanded content fade in. */ + val contentAfterFadeInDuration: Long + ) + + /** The interpolators used by this animator. */ + class Interpolators( + /** The interpolator used for the Y position, width, height and corner radius. */ + val positionInterpolator: Interpolator, + + /** + * The interpolator used for the X position. This can be different than + * [positionInterpolator] to create an arc-path during the animation. + */ + val positionXInterpolator: Interpolator = positionInterpolator, + + /** The interpolator used when fading out the expanding content. */ + val contentBeforeFadeOutInterpolator: Interpolator, + + /** The interpolator used when fading in the expanded content. */ + val contentAfterFadeInInterpolator: Interpolator + ) + /** * Start a launch animation controlled by [controller] towards [endState]. An intermediary * layer with [windowBackgroundColor] will fade in then fade out above the expanding view, and @@ -221,8 +251,8 @@ class LaunchAnimator @JvmOverloads constructor( // Update state. val animator = ValueAnimator.ofFloat(0f, 1f) - animator.duration = if (isForTesting) 0 else ANIMATION_DURATION - animator.interpolator = Interpolators.LINEAR + animator.duration = timings.totalDuration + animator.interpolator = LINEAR val launchContainerOverlay = launchContainer.overlay var cancelled = false @@ -260,8 +290,8 @@ class LaunchAnimator @JvmOverloads constructor( // TODO(b/184121838): Use reverse interpolators to get the same path/arc as the non // reversed animation. val linearProgress = animation.animatedFraction - val progress = animationInterpolator.getInterpolation(linearProgress) - val xProgress = animationInterpolatorX.getInterpolation(linearProgress) + val progress = interpolators.positionInterpolator.getInterpolation(linearProgress) + val xProgress = interpolators.positionXInterpolator.getInterpolation(linearProgress) val xCenter = MathUtils.lerp(startCenterX, endCenterX, xProgress) val halfWidth = MathUtils.lerp(startWidth, endWidth, progress) / 2f @@ -278,7 +308,12 @@ class LaunchAnimator @JvmOverloads constructor( // The expanding view can/should be hidden once it is completely covered by the opening // window. - state.visible = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) < 1 + state.visible = getProgress( + timings, + linearProgress, + timings.contentBeforeFadeOutDelay, + timings.contentBeforeFadeOutDuration + ) < 1 applyStateToWindowBackgroundLayer( windowBackgroundLayer, @@ -337,14 +372,25 @@ class LaunchAnimator @JvmOverloads constructor( // We first fade in the background layer to hide the expanding view, then fade it out // with SRC mode to draw a hole punch in the status bar and reveal the opening window. - val fadeInProgress = getProgress(linearProgress, 0, ANIMATION_DURATION_FADE_OUT_CONTENT) + val fadeInProgress = getProgress( + timings, + linearProgress, + timings.contentBeforeFadeOutDelay, + timings.contentBeforeFadeOutDuration + ) if (fadeInProgress < 1) { - val alpha = Interpolators.LINEAR_OUT_SLOW_IN.getInterpolation(fadeInProgress) + val alpha = + interpolators.contentBeforeFadeOutInterpolator.getInterpolation(fadeInProgress) drawable.alpha = (alpha * 0xFF).roundToInt() } else { val fadeOutProgress = getProgress( - linearProgress, ANIMATION_DELAY_FADE_IN_WINDOW, ANIMATION_DURATION_FADE_IN_WINDOW) - val alpha = 1 - WINDOW_FADE_IN_INTERPOLATOR.getInterpolation(fadeOutProgress) + timings, + linearProgress, + timings.contentAfterFadeInDelay, + timings.contentAfterFadeInDuration + ) + val alpha = + 1 - interpolators.contentAfterFadeInInterpolator.getInterpolation(fadeOutProgress) drawable.alpha = (alpha * 0xFF).roundToInt() if (drawHole) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java index 1d921702e6321..f2d926d97108a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java @@ -25,7 +25,6 @@ import android.service.dreams.IDreamManager; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; -import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -301,24 +300,15 @@ public interface StatusBarDependenciesModule { */ @Provides @SysUISingleton - static LaunchAnimator provideLaunchAnimator(Context context) { - return new LaunchAnimator(context); + static ActivityLaunchAnimator provideActivityLaunchAnimator() { + return new ActivityLaunchAnimator(); } /** */ @Provides @SysUISingleton - static ActivityLaunchAnimator provideActivityLaunchAnimator(LaunchAnimator launchAnimator) { - return new ActivityLaunchAnimator(launchAnimator); - } - - /** - */ - @Provides - @SysUISingleton - static DialogLaunchAnimator provideDialogLaunchAnimator(Context context, - LaunchAnimator launchAnimator, IDreamManager dreamManager) { - return new DialogLaunchAnimator(context, launchAnimator, dreamManager); + static DialogLaunchAnimator provideDialogLaunchAnimator(IDreamManager dreamManager) { + return new DialogLaunchAnimator(dreamManager); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt index 64a73054c4348..349b1918a504c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ExpandAnimationParameters.kt @@ -2,6 +2,7 @@ package com.android.systemui.statusbar.notification import android.util.MathUtils import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.animation.Interpolators import com.android.systemui.animation.LaunchAnimator import kotlin.math.min @@ -55,6 +56,7 @@ class ExpandAnimationParameters( } fun getProgress(delay: Long, duration: Long): Float { - return LaunchAnimator.getProgress(linearProgress, delay, duration) + return LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, linearProgress, delay, + duration) } } \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 0312c30c73ecc..c8bca7cfcd1c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -110,6 +110,7 @@ import com.android.keyguard.dagger.KeyguardUserSwitcherComponent; import com.android.systemui.DejankUtils; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.Interpolators; import com.android.systemui.animation.LaunchAnimator; import com.android.systemui.biometrics.AuthController; @@ -225,7 +226,8 @@ public class NotificationPanelViewController extends PanelViewController { */ private static final int FLING_HIDE = 2; private static final long ANIMATION_DELAY_ICON_FADE_IN = - LaunchAnimator.ANIMATION_DURATION - CollapsedStatusBarFragment.FADE_IN_DURATION + ActivityLaunchAnimator.TIMINGS.getTotalDuration() + - CollapsedStatusBarFragment.FADE_IN_DURATION - CollapsedStatusBarFragment.FADE_IN_DELAY - 48; private final DozeParameters mDozeParameters; @@ -3626,8 +3628,8 @@ public class NotificationPanelViewController extends PanelViewController { } public void applyLaunchAnimationProgress(float linearProgress) { - boolean hideIcons = LaunchAnimator.getProgress(linearProgress, - ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; + boolean hideIcons = LaunchAnimator.getProgress(ActivityLaunchAnimator.TIMINGS, + linearProgress, ANIMATION_DELAY_ICON_FADE_IN, 100) == 0.0f; if (hideIcons != mHideIconsDuringLaunchAnimation) { mHideIconsDuringLaunchAnimation = hideIcons; if (!hideIcons) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt index 32aae6c05df60..2ba37c2ec29f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt @@ -23,7 +23,8 @@ class StatusBarLaunchAnimatorController( delegate.onLaunchAnimationStart(isExpandingFullyAbove) statusBar.notificationPanelViewController.setIsLaunchAnimationRunning(true) if (!isExpandingFullyAbove) { - statusBar.collapsePanelWithDuration(LaunchAnimator.ANIMATION_DURATION.toInt()) + statusBar.collapsePanelWithDuration( + ActivityLaunchAnimator.TIMINGS.totalDuration.toInt()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt index d819fa2adc388..1fe3d44177303 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityLaunchAnimatorTest.kt @@ -46,7 +46,7 @@ import org.mockito.junit.MockitoJUnit @RunWithLooper class ActivityLaunchAnimatorTest : SysuiTestCase() { private val launchContainer = LinearLayout(mContext) - private val launchAnimator = LaunchAnimator(mContext, isForTesting = true) + private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS) @Mock lateinit var callback: ActivityLaunchAnimator.Callback @Spy private val controller = TestLaunchAnimatorController(launchContainer) @Mock lateinit var iCallback: IRemoteAnimationFinishedCallback diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt index ee1784d919d58..b951345a145bf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogLaunchAnimatorTest.kt @@ -33,7 +33,7 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class DialogLaunchAnimatorTest : SysuiTestCase() { - private val launchAnimator = LaunchAnimator(context, isForTesting = true) + private val launchAnimator = LaunchAnimator(TEST_TIMINGS, TEST_INTERPOLATORS) private lateinit var dialogLaunchAnimator: DialogLaunchAnimator private val attachedViews = mutableSetOf() @@ -43,7 +43,7 @@ class DialogLaunchAnimatorTest : SysuiTestCase() { @Before fun setUp() { dialogLaunchAnimator = DialogLaunchAnimator( - context, launchAnimator, dreamManager, isForTesting = true) + dreamManager, launchAnimator, isForTesting = true) } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt new file mode 100644 index 0000000000000..dadf94e2a9dda --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TestValues.kt @@ -0,0 +1,23 @@ +package com.android.systemui.animation + +/** + * A [LaunchAnimator.Timings] to be used in tests. + * + * Note that all timings except the total duration are non-zero to avoid divide-by-zero exceptions + * when computing the progress of a sub-animation (the contents fade in/out). + */ +val TEST_TIMINGS = LaunchAnimator.Timings( + totalDuration = 0L, + contentBeforeFadeOutDelay = 1L, + contentBeforeFadeOutDuration = 1L, + contentAfterFadeInDelay = 1L, + contentAfterFadeInDuration = 1L +) + +/** A [LaunchAnimator.Interpolators] to be used in tests. */ +val TEST_INTERPOLATORS = LaunchAnimator.Interpolators( + positionInterpolator = Interpolators.STANDARD, + positionXInterpolator = Interpolators.STANDARD, + contentBeforeFadeOutInterpolator = Interpolators.STANDARD, + contentAfterFadeInInterpolator = Interpolators.STANDARD +) \ No newline at end of file From 8c68088b6704623e8849d9c085e251fc08bdb941 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Thu, 2 Dec 2021 13:07:43 +0100 Subject: [PATCH 3/3] Change DialogLaunchAnimator X-axis interpolator This dialog changes the interpolator used to animate the X-axis position of dialogs during the launch animation. This avoid animating the dialog out of the screen bounds (see b/208241926#comment1). Bug: 208241926 Test: Manual Change-Id: Ia5accb747615fcc8410b5c488ff13617556305be --- .../android/systemui/animation/DialogLaunchAnimator.kt | 8 +++++++- .../src/com/android/systemui/animation/LaunchAnimator.kt | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index 1a6c66f383027..066e169dcde35 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -49,7 +49,13 @@ class DialogLaunchAnimator @JvmOverloads constructor( ) { private companion object { private val TIMINGS = ActivityLaunchAnimator.TIMINGS - private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS + + // We use the same interpolator for X and Y axis to make sure the dialog does not move out + // of the screen bounds during the animation. + private val INTERPOLATORS = ActivityLaunchAnimator.INTERPOLATORS.copy( + positionXInterpolator = ActivityLaunchAnimator.INTERPOLATORS.positionInterpolator + ) + private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt index fbd09c03afd7e..ebe96ebf29889 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/LaunchAnimator.kt @@ -172,7 +172,7 @@ class LaunchAnimator( ) /** The interpolators used by this animator. */ - class Interpolators( + data class Interpolators( /** The interpolator used for the Y position, width, height and corner radius. */ val positionInterpolator: Interpolator,