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

Commit cd2a1b2a authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

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
parent 0b229eae
Loading
Loading
Loading
Loading
+125 −46
Original line number Diff line number Diff line
@@ -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 {
        // 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 {
                    if (drawHostDialog) {
                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,17 +418,28 @@ private class DialogLaunchAnimation(
                oldBottom: Int
            ) {
                dialogView.removeOnLayoutChangeListener(this)

                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 = {
                        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)
                        }
                // 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)
@@ -398,8 +458,6 @@ private class DialogLaunchAnimation(
            }
        )
    }
        })
    }

    private fun onHostDialogDismissed(actualDismiss: () -> Unit) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
@@ -467,8 +525,26 @@ private class DialogLaunchAnimation(

                touchSurface.visibility = View.VISIBLE
                originalDialogView!!.visibility = View.INVISIBLE

                // 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) {