diff options
| author | 2021-10-11 14:59:02 +0200 | |
|---|---|---|
| committer | 2021-10-28 17:41:55 +0200 | |
| commit | cd2a1b2ad01b1ff37076da1ad8266d1a7d5b1bab (patch) | |
| tree | 20c8b12882907fe696ad56b0fcb37a579ca2d585 | |
| parent | 0b229eae8dc5c6c652fe5629b1aaeab0007a94cd (diff) | |
Synchronize dialog launch animations
This CL adds some synchronization to the dialog launch animation to
avoid flickering at the beginning and the end of the animation. It does
so by drawing the touch surface twice (in both the original window and
the dialog window) using a temporary ghost that is added/removed
before/after the animation.
I decided not to reuse the ghosts created by
GhostedViewLaunchAnimatorController because this would make this change
much more invasive, which I wanted to avoid given that the end goal is
to use BLAST synchronization instead of this CL.
Change-Id: Iac2eb2a2e78801a43847eebc72679c4952a73f1f
Bug: 193634619
Test: Manual
| -rw-r--r-- | packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt | 171 |
1 files changed, 125 insertions, 46 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 dbb5831c9d47..669a054eaa2a 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -21,10 +21,11 @@ import android.content.Context import android.graphics.Color import android.os.Looper import android.util.Log +import android.view.GhostView import android.view.Gravity import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver +import android.view.ViewTreeObserver.OnPreDrawListener import android.view.WindowInsets import android.view.WindowManager import android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR @@ -225,10 +226,12 @@ private class DialogLaunchAnimation( private var isDismissing = false private var dismissRequested = false - private var drawHostDialog = false var ignoreNextCallToHide = false var exitAnimationDisabled = false + private var isTouchSurfaceGhostDrawn = false + private var isOriginalDialogViewLaidOut = false + fun start() { // Show the host (fullscreen) dialog, to which we will add the stolen dialog view. hostDialog.show() @@ -267,19 +270,65 @@ private class DialogLaunchAnimation( window.setDecorFitsSystemWindows(false) } - // Prevent the host dialog from drawing until the animation starts. - hostDialogRoot.viewTreeObserver.addOnPreDrawListener( - object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - if (drawHostDialog) { - hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) - return true - } + // Disable the dim. We will enable it once we start the animation. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + + // 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 host 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). + hostDialogRoot.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener { + override fun onPreDraw(): Boolean { + hostDialogRoot.viewTreeObserver.removeOnPreDrawListener(this) + addTemporaryTouchSurfaceGhost() + return true + } + }) + hostDialogRoot.invalidate() + } - return false - } + private fun addTemporaryTouchSurfaceGhost() { + // Create a ghost of the touch surface (which will make the touch surface invisible) and add + // it to the host dialog. We will wait for this ghost to be drawn before starting the + // animation. + val ghost = GhostView.addGhost(touchSurface, hostDialogRoot) + + // 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 } - ) + }) + ghost.invalidate() + } + + 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 + if (touchSurface is LaunchableView) { + touchSurface.setShouldBlockVisibilityChanges(true) + } + + // 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 + } + }) + touchSurface.invalidate() } /** Get the content view of [originalDialog] and pass it to [then]. */ @@ -291,7 +340,7 @@ private class DialogLaunchAnimation( ?: throw IllegalStateException("Dialog does not have any android.R.id.content view") androidContent.viewTreeObserver.addOnPreDrawListener( - object : ViewTreeObserver.OnPreDrawListener { + object : OnPreDrawListener { override fun onPreDraw(): Boolean { if (androidContent.childCount == 1) { androidContent.viewTreeObserver.removeOnPreDrawListener(this) @@ -369,38 +418,47 @@ private class DialogLaunchAnimation( oldBottom: Int ) { dialogView.removeOnLayoutChangeListener(this) - startAnimation( - isLaunching = true, - onLaunchAnimationStart = { - drawHostDialog = true - - // 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. - if (touchSurface is LaunchableView) { - touchSurface.setShouldBlockVisibilityChanges(true) - } - }, - onLaunchAnimationEnd = { - touchSurface.setTag(R.id.launch_animation_running, null) - - // We hide the touch surface when the dialog is showing. We will make this - // view visible again when dismissing the dialog. - touchSurface.visibility = View.INVISIBLE - - isLaunching = false - - // dismiss was called during the animation, dismiss again now to actually - // dismiss. - if (dismissRequested) { - hostDialog.dismiss() - } - } - ) + + isOriginalDialogViewLaidOut = true + maybeStartLaunchAnimation() } }) } + private fun maybeStartLaunchAnimation() { + if (!isTouchSurfaceGhostDrawn || !isOriginalDialogViewLaidOut) { + return + } + + // Show the background dim. + hostDialog.window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + + startAnimation( + isLaunching = true, + onLaunchAnimationStart = { + // Remove the temporary ghost. Another ghost (that ghosts only the touch surface + // content, and not its background) will be added right after this and will be + // animated. + GhostView.removeGhost(touchSurface) + }, + onLaunchAnimationEnd = { + touchSurface.setTag(R.id.launch_animation_running, null) + + // We hide the touch surface when the dialog is showing. We will make this + // view visible again when dismissing the dialog. + touchSurface.visibility = View.INVISIBLE + + isLaunching = false + + // dismiss was called during the animation, dismiss again now to actually + // dismiss. + if (dismissRequested) { + hostDialog.dismiss() + } + } + ) + } + private fun onHostDialogDismissed(actualDismiss: () -> Unit) { if (Looper.myLooper() != Looper.getMainLooper()) { context.mainExecutor.execute { onHostDialogDismissed(actualDismiss) } @@ -467,8 +525,26 @@ private class DialogLaunchAnimation( touchSurface.visibility = View.VISIBLE originalDialogView!!.visibility = View.INVISIBLE - dismissDialogs(true /* instantDismiss */) - onDialogDismissed(this@DialogLaunchAnimation) + + // 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, hostDialogRoot) + + 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) + dismissDialogs(true /* instantDismiss */) + onDialogDismissed(this@DialogLaunchAnimation) + + return true + } + }) + touchSurface.invalidate() } ) } @@ -503,10 +579,13 @@ private class DialogLaunchAnimation( } override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) { + // During launch, onLaunchAnimationStart will be used to remove the temporary touch + // surface ghost so it is important to call this before calling + // onLaunchAnimationStart on the controller (which will create its own ghost). + onLaunchAnimationStart() + startViewController.onLaunchAnimationStart(isExpandingFullyAbove) endViewController.onLaunchAnimationStart(isExpandingFullyAbove) - - onLaunchAnimationStart() } override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) { |