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

Commit a797d098 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge "Animate dialog to app launches (1/2)"

parents a6c96b92 6e77c58d
Loading
Loading
Loading
Loading
+1 −1
Original line number Original line Diff line number Diff line
@@ -39,5 +39,5 @@ android_library {
    ],
    ],


    manifest: "AndroidManifest.xml",
    manifest: "AndroidManifest.xml",
    kotlincflags: ["-Xjvm-default=enable"],
    kotlincflags: ["-Xjvm-default=all"],
}
}
+55 −6
Original line number Original line Diff line number Diff line
@@ -48,9 +48,16 @@ private const val TAG = "ActivityLaunchAnimator"
 * nicely into the starting window.
 * nicely into the starting window.
 */
 */
class ActivityLaunchAnimator(
class ActivityLaunchAnimator(
    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS)
    /** The animator used when animating a View into an app. */
    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),

    /** The animator used when animating a Dialog into an app. */
    // TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
    // TIMINGS.contentBeforeFadeOutDuration.
    private val dialogToAppAnimator: LaunchAnimator = LaunchAnimator(DIALOG_TIMINGS, INTERPOLATORS)
) {
) {
    companion object {
    companion object {
        /** The timings when animating a View into an app. */
        @JvmField
        @JvmField
        val TIMINGS = LaunchAnimator.Timings(
        val TIMINGS = LaunchAnimator.Timings(
            totalDuration = 500L,
            totalDuration = 500L,
@@ -60,6 +67,17 @@ class ActivityLaunchAnimator(
            contentAfterFadeInDuration = 183L
            contentAfterFadeInDuration = 183L
        )
        )


        /**
         * The timings when animating a Dialog into an app. We need to wait at least 200ms before
         * showing the app (which is under the dialog window) so that the dialog window dim is fully
         * faded out, to avoid flicker.
         */
        val DIALOG_TIMINGS = TIMINGS.copy(
            contentBeforeFadeOutDuration = 200L,
            contentAfterFadeInDelay = 200L
        )

        /** The interpolators when animating a View or a dialog into an app. */
        val INTERPOLATORS = LaunchAnimator.Interpolators(
        val INTERPOLATORS = LaunchAnimator.Interpolators(
            positionInterpolator = Interpolators.EMPHASIZED,
            positionInterpolator = Interpolators.EMPHASIZED,
            positionXInterpolator = createPositionXInterpolator(),
            positionXInterpolator = createPositionXInterpolator(),
@@ -297,11 +315,18 @@ class ActivityLaunchAnimator(
            }
            }
        }
        }


        /**
         * Whether this controller is controlling a dialog launch. This will be used to adapt the
         * timings, making sure we don't show the app until the dialog dim had the time to fade out.
         */
        // TODO(b/218989950): Remove this.
        val isDialogLaunch: Boolean
            get() = false

        /**
        /**
         * The intent was started. If [willAnimate] is false, nothing else will happen and the
         * The intent was started. If [willAnimate] is false, nothing else will happen and the
         * animation will not be started.
         * animation will not be started.
         */
         */
        @JvmDefault
        fun onIntentStarted(willAnimate: Boolean) {}
        fun onIntentStarted(willAnimate: Boolean) {}


        /**
        /**
@@ -309,7 +334,6 @@ class ActivityLaunchAnimator(
         * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
         * this if the animation was already started, i.e. if [onLaunchAnimationStart] was called
         * before the cancellation.
         * before the cancellation.
         */
         */
        @JvmDefault
        fun onLaunchAnimationCancelled() {}
        fun onLaunchAnimationCancelled() {}
    }
    }


@@ -317,7 +341,9 @@ class ActivityLaunchAnimator(
    inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
    inner class Runner(private val controller: Controller) : IRemoteAnimationRunner.Stub() {
        private val launchContainer = controller.launchContainer
        private val launchContainer = controller.launchContainer
        private val context = launchContainer.context
        private val context = launchContainer.context
        private val transactionApplier = SyncRtSurfaceTransactionApplier(launchContainer)
        private val transactionApplierView =
            controller.openingWindowSyncView ?: controller.launchContainer
        private val transactionApplier = SyncRtSurfaceTransactionApplier(transactionApplierView)


        private val matrix = Matrix()
        private val matrix = Matrix()
        private val invertMatrix = Matrix()
        private val invertMatrix = Matrix()
@@ -405,6 +431,13 @@ class ActivityLaunchAnimator(
            val callback = this@ActivityLaunchAnimator.callback!!
            val callback = this@ActivityLaunchAnimator.callback!!
            val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)
            val windowBackgroundColor = callback.getBackgroundColor(window.taskInfo)


            // Make sure we use the modified timings when animating a dialog into an app.
            val launchAnimator = if (controller.isDialogLaunch) {
                dialogToAppAnimator
            } else {
                launchAnimator
            }

            // TODO(b/184121838): We should somehow get the top and bottom radius of the window
            // TODO(b/184121838): We should somehow get the top and bottom radius of the window
            // instead of recomputing isExpandingFullyAbove here.
            // instead of recomputing isExpandingFullyAbove here.
            val isExpandingFullyAbove =
            val isExpandingFullyAbove =
@@ -440,19 +473,29 @@ class ActivityLaunchAnimator(
                    progress: Float,
                    progress: Float,
                    linearProgress: Float
                    linearProgress: Float
                ) {
                ) {
                    // Apply the state to the window only if it is visible, i.e. when the expanding
                    // view is *not* visible.
                    if (!state.visible) {
                        applyStateToWindow(window, state)
                        applyStateToWindow(window, state)
                    }
                    navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }
                    navigationBar?.let { applyStateToNavigationBar(it, state, linearProgress) }

                    listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
                    listeners.forEach { it.onLaunchAnimationProgress(linearProgress) }
                    delegate.onLaunchAnimationProgress(state, progress, linearProgress)
                    delegate.onLaunchAnimationProgress(state, progress, linearProgress)
                }
                }
            }
            }


            // We draw a hole when the additional layer is fading out to reveal the opening window.
            animation = launchAnimator.startAnimation(
            animation = launchAnimator.startAnimation(
                controller, endState, windowBackgroundColor, drawHole = true)
                controller, endState, windowBackgroundColor, drawHole = true)
        }
        }


        private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
        private fun applyStateToWindow(window: RemoteAnimationTarget, state: LaunchAnimator.State) {
            if (transactionApplierView.viewRootImpl == null) {
                // If the view root we synchronize with was detached, don't apply any transaction
                // (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
                return
            }

            val screenBounds = window.screenSpaceBounds
            val screenBounds = window.screenSpaceBounds
            val centerX = (screenBounds.left + screenBounds.right) / 2f
            val centerX = (screenBounds.left + screenBounds.right) / 2f
            val centerY = (screenBounds.top + screenBounds.bottom) / 2f
            val centerY = (screenBounds.top + screenBounds.bottom) / 2f
@@ -510,6 +553,12 @@ class ActivityLaunchAnimator(
            state: LaunchAnimator.State,
            state: LaunchAnimator.State,
            linearProgress: Float
            linearProgress: Float
        ) {
        ) {
            if (transactionApplierView.viewRootImpl == null) {
                // If the view root we synchronize with was detached, don't apply any transaction
                // (as [SyncRtSurfaceTransactionApplier.scheduleApply] would otherwise throw).
                return
            }

            val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
            val fadeInProgress = LaunchAnimator.getProgress(TIMINGS, linearProgress,
                ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)
                ANIMATION_DELAY_NAV_FADE_IN, ANIMATION_DURATION_NAV_FADE_OUT)


+122 −42
Original line number Original line Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.animation
import android.animation.Animator
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.animation.ValueAnimator
import android.app.ActivityManager
import android.app.Dialog
import android.app.Dialog
import android.graphics.Color
import android.graphics.Color
import android.graphics.Rect
import android.graphics.Rect
@@ -28,12 +27,12 @@ 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.ViewRootImpl
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
import android.widget.FrameLayout
import android.widget.FrameLayout
import kotlin.math.roundToInt
import kotlin.math.roundToInt


@@ -42,12 +41,17 @@ 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.
 *
 * This animator also allows to easily animate a dialog into an activity.
 *
 * @see showFromView
 * @see showFromDialog
 * @see createActivityLaunchController
 */
 */
class DialogLaunchAnimator @JvmOverloads constructor(
class DialogLaunchAnimator @JvmOverloads constructor(
    private val dreamManager: IDreamManager,
    private val dreamManager: IDreamManager,
    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
    private val launchAnimator: LaunchAnimator = LaunchAnimator(TIMINGS, INTERPOLATORS),
    // TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
    private val isForTesting: Boolean = false
    private var forceDisableSynchronization: Boolean = ActivityManager.isLowRamDeviceStatic()
) {
) {
    private companion object {
    private companion object {
        private val TIMINGS = ActivityLaunchAnimator.TIMINGS
        private val TIMINGS = ActivityLaunchAnimator.TIMINGS
@@ -113,7 +117,7 @@ class DialogLaunchAnimator @JvmOverloads constructor(
                dialog = dialog,
                dialog = dialog,
                animateBackgroundBoundsChange,
                animateBackgroundBoundsChange,
                animatedParent,
                animatedParent,
                forceDisableSynchronization
                isForTesting
        )
        )


        openedDialogs.add(animatedDialog)
        openedDialogs.add(animatedDialog)
@@ -140,6 +144,100 @@ class DialogLaunchAnimator @JvmOverloads constructor(
        showFromView(dialog, view, animateBackgroundBoundsChange)
        showFromView(dialog, view, animateBackgroundBoundsChange)
    }
    }


    /**
     * Create an [ActivityLaunchAnimator.Controller] that can be used to launch an activity from the
     * dialog that contains [View]. Note that the dialog must have been show using [showFromView]
     * and be currently showing, otherwise this will return null.
     *
     * The returned controller will take care of dismissing the dialog at the right time after the
     * activity started, when the dialog to app animation is done (or when it is cancelled). If this
     * method returns null, then the dialog won't be dismissed.
     *
     * @param view any view inside the dialog to animate.
     */
    @JvmOverloads
    fun createActivityLaunchController(
        view: View,
        cujType: Int? = null
    ): ActivityLaunchAnimator.Controller? {
        val animatedDialog = openedDialogs
            .firstOrNull { it.dialog.window.decorView.viewRootImpl == view.viewRootImpl }
            ?: return null

        // At this point, we know that the intent of the caller is to dismiss the dialog to show
        // an app, so we disable the exit animation into the touch surface because we will never
        // want to run it anyways.
        animatedDialog.exitAnimationDisabled = true

        val dialog = animatedDialog.dialog

        // Don't animate if the dialog is not showing.
        if (!dialog.isShowing) {
            return null
        }

        val dialogContentWithBackground = animatedDialog.dialogContentWithBackground ?: return null
        val controller =
            ActivityLaunchAnimator.Controller.fromView(dialogContentWithBackground, cujType)
                ?: return null

        // Wrap the controller into one that will instantly dismiss the dialog when the animation is
        // done or dismiss it normally (fading it out) if the animation is cancelled.
        return object : ActivityLaunchAnimator.Controller by controller {
            override val isDialogLaunch = true

            override fun onIntentStarted(willAnimate: Boolean) {
                controller.onIntentStarted(willAnimate)

                if (!willAnimate) {
                    dialog.dismiss()
                }
            }

            override fun onLaunchAnimationCancelled() {
                controller.onLaunchAnimationCancelled()
                enableDialogDismiss()
                dialog.dismiss()
            }

            override fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {
                controller.onLaunchAnimationStart(isExpandingFullyAbove)

                // Make sure the dialog is not dismissed during the animation.
                disableDialogDismiss()

                // If this dialog was shown from a cascade of other dialogs, make sure those ones
                // are dismissed too.
                animatedDialog.touchSurface = animatedDialog.prepareForStackDismiss()

                // Remove the dim.
                dialog.window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
            }

            override fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {
                controller.onLaunchAnimationEnd(isExpandingFullyAbove)

                // Hide the dialog then dismiss it to instantly dismiss it without playing the
                // animation.
                dialog.hide()
                enableDialogDismiss()
                dialog.dismiss()
            }

            private fun disableDialogDismiss() {
                dialog.setDismissOverride { /* Do nothing */ }
            }

            private fun enableDialogDismiss() {
                // We don't set the override to null given that [AnimatedDialog.OnDialogDismissed]
                // will still properly dismiss the dialog but will also make sure to clean up
                // everything (like making sure that the touched view that triggered the dialog is
                // made VISIBLE again).
                dialog.setDismissOverride(animatedDialog::onDialogDismissed)
            }
        }
    }

    /**
    /**
     * Ensure that all dialogs currently shown won't animate into their touch surface when
     * Ensure that all dialogs currently shown won't animate into their touch surface when
     * dismissed.
     * dismissed.
@@ -358,6 +456,21 @@ private class AnimatedDialog(
        // Make sure the dialog is visible instantly and does not do any window animation.
        // Make sure the dialog is visible instantly and does not do any window animation.
        window.attributes.windowAnimations = R.style.Animation_LaunchAnimation
        window.attributes.windowAnimations = R.style.Animation_LaunchAnimation


        // Ensure that the animation is not clipped by the display cut-out when animating this
        // dialog into an app.
        window.attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
        window.attributes = window.attributes

        // We apply the insets ourselves to make sure that the paddings are set on the correct
        // View.
        window.setDecorFitsSystemWindows(false)
        val viewWithInsets = (dialogContentWithBackground.parent as ViewGroup)
        viewWithInsets.setOnApplyWindowInsetsListener { view, windowInsets ->
            val insets = windowInsets.getInsets(WindowInsets.Type.displayCutout())
            view.setPadding(insets.left, insets.top, insets.right, insets.bottom)
            WindowInsets.CONSUMED
        }

        // Start the animation once the background view is properly laid out.
        // Start the animation once the background view is properly laid out.
        dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
        dialogContentWithBackground.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
            override fun onLayoutChange(
            override fun onLayoutChange(
@@ -421,45 +534,12 @@ private class AnimatedDialog(
     * (or inversely, removed from the UI when the touch surface is made visible).
     * (or inversely, removed from the UI when the touch surface is made visible).
     */
     */
    private fun synchronizeNextDraw(then: () -> Unit) {
    private fun synchronizeNextDraw(then: () -> Unit) {
        if (forceDisableSynchronization ||
        if (forceDisableSynchronization) {
                !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()
            then()
            return
            return
        }
        }


        // Consume the next frames of both view roots to make sure the ghost view is drawn at
        ViewRootSync.synchronizeNextDraw(touchSurface, decorView, then)
        // 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()
            }
        }

        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? {
    private fun findFirstViewGroupWithBackground(view: View): ViewGroup? {
@@ -523,7 +603,7 @@ private class AnimatedDialog(
        )
        )
    }
    }


    private fun onDialogDismissed() {
    fun onDialogDismissed() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            dialog.context.mainExecutor.execute { onDialogDismissed() }
            dialog.context.mainExecutor.execute { onDialogDismissed() }
            return
            return
+47 −7
Original line number Original line Diff line number Diff line
@@ -77,8 +77,8 @@ class LaunchAnimator(
         * This will be used to:
         * This will be used to:
         *  - Get the associated [Context].
         *  - Get the associated [Context].
         *  - Compute whether we are expanding fully above the launch container.
         *  - Compute whether we are expanding fully above the launch container.
         *  - Apply surface transactions in sync with RenderThread when animating an activity
         *  - Get to overlay to which we initially put the window background layer, until the
         *    launch.
         *    opening window is made visible (see [openingWindowSyncView]).
         *
         *
         * This container can be changed to force this [Controller] to animate the expanding view
         * This container can be changed to force this [Controller] to animate the expanding view
         * inside a different location, for instance to ensure correct layering during the
         * inside a different location, for instance to ensure correct layering during the
@@ -86,6 +86,18 @@ class LaunchAnimator(
         */
         */
        var launchContainer: ViewGroup
        var launchContainer: ViewGroup


        /**
         * The [View] with which the opening app window should be synchronized with once it starts
         * to be visible.
         *
         * We will also move the window background layer to this view's overlay once the opening
         * window is visible.
         *
         * If null, this will default to [launchContainer].
         */
        val openingWindowSyncView: View?
            get() = null

        /**
        /**
         * Return the [State] of the view that will be animated. We will animate from this state to
         * Return the [State] of the view that will be animated. We will animate from this state to
         * the final window state.
         * the final window state.
@@ -100,11 +112,9 @@ class LaunchAnimator(
         * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
         * needed for the animation. [isExpandingFullyAbove] will be true if the window is expanding
         * fully above the [launchContainer].
         * fully above the [launchContainer].
         */
         */
        @JvmDefault
        fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}
        fun onLaunchAnimationStart(isExpandingFullyAbove: Boolean) {}


        /** The animation made progress and the expandable view [state] should be updated. */
        /** The animation made progress and the expandable view [state] should be updated. */
        @JvmDefault
        fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}
        fun onLaunchAnimationProgress(state: State, progress: Float, linearProgress: Float) {}


        /**
        /**
@@ -112,7 +122,6 @@ class LaunchAnimator(
         * called previously. This is typically used to clean up the resources initialized when the
         * called previously. This is typically used to clean up the resources initialized when the
         * animation was started.
         * animation was started.
         */
         */
        @JvmDefault
        fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
        fun onLaunchAnimationEnd(isExpandingFullyAbove: Boolean) {}
    }
    }


@@ -154,7 +163,7 @@ class LaunchAnimator(
    }
    }


    /** The timings (durations and delays) used by this animator. */
    /** The timings (durations and delays) used by this animator. */
    class Timings(
    data class Timings(
        /** The total duration of the animation. */
        /** The total duration of the animation. */
        val totalDuration: Long,
        val totalDuration: Long,


@@ -257,8 +266,17 @@ class LaunchAnimator(
        animator.duration = timings.totalDuration
        animator.duration = timings.totalDuration
        animator.interpolator = LINEAR
        animator.interpolator = LINEAR


        // Whether we should move the [windowBackgroundLayer] into the overlay of
        // [Controller.openingWindowSyncView] once the opening app window starts to be visible.
        val openingWindowSyncView = controller.openingWindowSyncView
        val openingWindowSyncViewOverlay = openingWindowSyncView?.overlay
        val moveBackgroundLayerWhenAppIsVisible = openingWindowSyncView != null &&
            openingWindowSyncView.viewRootImpl != controller.launchContainer.viewRootImpl

        val launchContainerOverlay = launchContainer.overlay
        val launchContainerOverlay = launchContainer.overlay
        var cancelled = false
        var cancelled = false
        var movedBackgroundLayer = false

        animator.addListener(object : AnimatorListenerAdapter() {
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
            override fun onAnimationStart(animation: Animator?, isReverse: Boolean) {
                if (DEBUG) {
                if (DEBUG) {
@@ -278,6 +296,10 @@ class LaunchAnimator(
                }
                }
                controller.onLaunchAnimationEnd(isExpandingFullyAbove)
                controller.onLaunchAnimationEnd(isExpandingFullyAbove)
                launchContainerOverlay.remove(windowBackgroundLayer)
                launchContainerOverlay.remove(windowBackgroundLayer)

                if (moveBackgroundLayerWhenAppIsVisible) {
                    openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
                }
            }
            }
        })
        })


@@ -318,11 +340,29 @@ class LaunchAnimator(
                timings.contentBeforeFadeOutDuration
                timings.contentBeforeFadeOutDuration
            ) < 1
            ) < 1


            if (moveBackgroundLayerWhenAppIsVisible && !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

                launchContainerOverlay.remove(windowBackgroundLayer)
                openingWindowSyncViewOverlay!!.add(windowBackgroundLayer)

                ViewRootSync.synchronizeNextDraw(launchContainer, openingWindowSyncView, then = {})
            }

            val container = if (movedBackgroundLayer) {
                openingWindowSyncView!!
            } else {
                controller.launchContainer
            }

            applyStateToWindowBackgroundLayer(
            applyStateToWindowBackgroundLayer(
                windowBackgroundLayer,
                windowBackgroundLayer,
                state,
                state,
                linearProgress,
                linearProgress,
                launchContainer,
                container,
                drawHole
                drawHole
            )
            )
            controller.onLaunchAnimationProgress(state, progress, linearProgress)
            controller.onLaunchAnimationProgress(state, progress, linearProgress)
+75 −0
Original line number Original line Diff line number Diff line
package com.android.systemui.animation

import android.app.ActivityManager
import android.view.SurfaceControl
import android.view.View
import android.view.ViewRootImpl

/** A util class to synchronize 2 view roots. */
// TODO(b/200284684): Remove this class.
object ViewRootSync {
    // TODO(b/217621394): Remove special handling for low-RAM devices after animation sync is fixed
    private val forceDisableSynchronization = ActivityManager.isLowRamDeviceStatic()

    /**
     * Synchronize the next draw between the view roots of [view] and [otherView], then run [then].
     *
     * Note that in some cases, the synchronization might not be possible (e.g. WM consumed the
     * next transactions) or disabled (temporarily, on low ram devices). In this case, [then] will
     * be called without synchronizing.
     */
    fun synchronizeNextDraw(
        view: View,
        otherView: View,
        then: () -> Unit
    ) {
        if (forceDisableSynchronization ||
            !view.isAttachedToWindow || view.viewRootImpl == null ||
            !otherView.isAttachedToWindow || otherView.viewRootImpl == null ||
            view.viewRootImpl == otherView.viewRootImpl) {
            // 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()
            }
        }

        fun consumeNextDraw(viewRootImpl: ViewRootImpl) {
            if (viewRootImpl.consumeNextDraw(::onTransaction)) {
                remainingTransactions++

                // Make sure we trigger a traversal.
                viewRootImpl.view.invalidate()
            }
        }

        consumeNextDraw(view.viewRootImpl)
        consumeNextDraw(otherView.viewRootImpl)

        if (remainingTransactions == 0) {
            then()
        }
    }

    /**
     * A Java-friendly API for [synchronizeNextDraw].
     */
    @JvmStatic
    fun synchronizeNextDraw(view: View, otherView: View, then: Runnable) {
        synchronizeNextDraw(view, otherView, then::run)
    }
}
 No newline at end of file
Loading