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

Commit 9ba35a27 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

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
parent 2b9624f8
Loading
Loading
Loading
Loading
+82 −68
Original line number Original line Diff line number Diff line
@@ -28,10 +28,11 @@ import android.service.dreams.IDreamManager
import android.util.Log
import android.util.Log
import android.util.MathUtils
import android.util.MathUtils
import android.view.GhostView
import android.view.GhostView
import android.view.SurfaceControl
import android.view.View
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewTreeObserver.OnPreDrawListener
import android.view.ViewRootImpl
import android.view.WindowManager
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.FrameLayout
import kotlin.math.roundToInt
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
 * A class that allows dialogs to be started in a seamless way from a view that is transforming
 * nicely into the starting dialog.
 * nicely into the starting dialog.
 */
 */
class DialogLaunchAnimator(
class DialogLaunchAnimator @JvmOverloads constructor(
    private val context: Context,
    private val context: Context,
    private val launchAnimator: LaunchAnimator,
    private val launchAnimator: LaunchAnimator,
    private val dreamManager: IDreamManager
    private val dreamManager: IDreamManager,
    private var isForTesting: Boolean = false
) {
) {
    private companion object {
    private companion object {
        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
        private val TAG_LAUNCH_ANIMATION_RUNNING = R.id.launch_animation_running
@@ -103,7 +105,8 @@ class DialogLaunchAnimator(
                onDialogDismissed = { openedDialogs.remove(it) },
                onDialogDismissed = { openedDialogs.remove(it) },
                dialog = dialog,
                dialog = dialog,
                animateBackgroundBoundsChange,
                animateBackgroundBoundsChange,
                animatedParent
                animatedParent,
                isForTesting
        )
        )


        openedDialogs.add(animatedDialog)
        openedDialogs.add(animatedDialog)
@@ -177,7 +180,13 @@ private class AnimatedDialog(
    private val animateBackgroundBoundsChange: Boolean,
    private val animateBackgroundBoundsChange: Boolean,


    /** Launch animation corresponding to the parent [AnimatedDialog]. */
    /** 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.
     * The DecorView of this dialog window.
@@ -365,59 +374,77 @@ private class AnimatedDialog(
        // Show the dialog.
        // Show the dialog.
        dialog.show()
        dialog.show()


        // Add a temporary touch surface ghost as soon as the window is ready to draw. This
        addTouchSurfaceGhost()
        // 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()
    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
        }
        }


    private fun addTemporaryTouchSurfaceGhost() {
        // Create a ghost of the touch surface (which will make the touch surface invisible) and add
        // 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.
        // it to the host dialog. We trigger a one off synchronization to make sure that this is
        val ghost = GhostView.addGhost(touchSurface, decorView)
        // done in sync between the two different windows.
        synchronizeNextDraw(then = {
            isTouchSurfaceGhostDrawn = true
            maybeStartLaunchAnimation()
        })
        GhostView.addGhost(touchSurface, decorView)


        // The ghost of the touch surface was just created, so the touch surface was made invisible.
        // The ghost of the touch surface was just created, so the touch surface is currently
        // We make it visible again until the ghost is actually drawn.
        // invisible. We need to make sure that it stays invisible as long as the dialog is shown or
        touchSurface.visibility = View.VISIBLE
        // animating.
        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)
    }


        // Wait for the ghost to be drawn before continuing.
    /**
        ghost.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
     * Synchronize the next draw of the touch surface and dialog view roots so that they are
            override fun onPreDraw(): Boolean {
     * performed at the same time, in the same transaction. This is necessary to make sure that the
                ghost.viewTreeObserver.removeOnPreDrawListener(this)
     * ghost of the touch surface is drawn at the same time as the touch surface is made invisible
                onTouchSurfaceGhostDrawn()
     * (or inversely, removed from the UI when the touch surface is made visible).
                return true
     */
    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
        }

        // 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()
            }
            }
        })
        ghost.invalidate()
        }
        }


    private fun onTouchSurfaceGhostDrawn() {
        fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
        // Make the touch surface invisible and make sure that it stays invisible as long as the
            if (viewRootImpl.consumeNextDraw(::onTransaction)) {
        // dialog is shown or animating.
                remainingTransactions++
        touchSurface.visibility = View.INVISIBLE
        (touchSurface as? LaunchableView)?.setShouldBlockVisibilityChanges(true)


        // Add a pre draw listener to (maybe) start the animation once the touch surface is
                // Make sure we trigger a traversal.
        // actually invisible.
                viewRootImpl.view.invalidate()
        touchSurface.viewTreeObserver.addOnPreDrawListener(object : OnPreDrawListener {
            }
            override fun onPreDraw(): Boolean {
        }
                touchSurface.viewTreeObserver.removeOnPreDrawListener(this)

                isTouchSurfaceGhostDrawn = true
        consumeNextDraw(touchSurface.viewRootImpl)
                maybeStartLaunchAnimation()
        consumeNextDraw(decorView.viewRootImpl)
                return true

        if (remainingTransactions == 0) {
            then()
        }
        }
        })
        touchSurface.invalidate()
    }
    }


    private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
    private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -556,25 +583,12 @@ private class AnimatedDialog(
                        .removeOnLayoutChangeListener(backgroundLayoutListener)
                        .removeOnLayoutChangeListener(backgroundLayoutListener)
                }
                }


                // The animated ghost was just removed. We create a temporary ghost that will be
                // Make sure that the removal of the ghost and making the touch surface visible is
                // removed only once we draw the touch surface, to avoid flickering that would
                // done at the same time.
                // happen when removing the ghost too early (before the touch surface is drawn).
                synchronizeNextDraw(then = {
                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 */)
                    onAnimationFinished(true /* instantDismiss */)
                    onDialogDismissed(this@AnimatedDialog)
                    onDialogDismissed(this@AnimatedDialog)

                        return true
                    }
                })
                })
                touchSurface.invalidate()
            }
            }
        )
        )
    }
    }
+2 −1
Original line number Original line Diff line number Diff line
@@ -42,7 +42,8 @@ class DialogLaunchAnimatorTest : SysuiTestCase() {


    @Before
    @Before
    fun setUp() {
    fun setUp() {
        dialogLaunchAnimator = DialogLaunchAnimator(context, launchAnimator, dreamManager)
        dialogLaunchAnimator = DialogLaunchAnimator(
            context, launchAnimator, dreamManager, isForTesting = true)
    }
    }


    @After
    @After