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

Commit 51fa0117 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Rework Volume Dialog appear animation using DynamicAnimation" into main

parents 5e6a3f32 6a238005
Loading
Loading
Loading
Loading
+48 −47
Original line number Diff line number Diff line
@@ -17,18 +17,20 @@
package com.android.systemui.volume.dialog.ui.binder

import android.app.Dialog
import android.content.res.Resources
import android.view.View
import android.view.ViewTreeObserver
import android.view.WindowInsets
import androidx.compose.ui.util.lerp
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.updatePadding
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.view.RotationPolicy
import com.android.systemui.common.ui.view.onApplyWindowInsets
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.volume.SystemUIInterpolators
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory
@@ -36,6 +38,7 @@ import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel
import com.android.systemui.volume.dialog.utils.VolumeTracer
import javax.inject.Inject
import kotlin.math.ceil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -47,24 +50,25 @@ import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine

private const val SPRING_STIFFNESS = 700f
private const val SPRING_DAMPING_RATIO = 0.9f

private const val FRACTION_HIDE = 0f
private const val FRACTION_SHOW = 1f
private const val ANIMATION_MINIMUM_VISIBLE_CHANGE = 0.01f

/** Binds the root view of the Volume Dialog. */
@OptIn(ExperimentalCoroutinesApi::class)
@VolumeDialogScope
class VolumeDialogViewBinder
@Inject
constructor(
    @Main resources: Resources,
    private val viewModel: VolumeDialogViewModel,
    private val jankListenerFactory: JankListenerFactory,
    private val tracer: VolumeTracer,
    private val viewBinders: List<@JvmSuppressWildcards ViewBinder>,
) {

    private val dialogShowAnimationDurationMs =
        resources.getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong()
    private val dialogHideAnimationDurationMs =
        resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong()

    fun CoroutineScope.bind(dialog: Dialog) {
        val insets: MutableStateFlow<WindowInsets> =
            MutableStateFlow(WindowInsets.Builder().build())
@@ -110,22 +114,35 @@ constructor(
        dialog: Dialog,
        visibilityModel: Flow<VolumeDialogVisibilityModel>,
    ) {
        view.applyAnimationProgress(FRACTION_HIDE)
        val animationValueHolder = FloatValueHolder(FRACTION_HIDE)
        val animation: SpringAnimation =
            SpringAnimation(animationValueHolder)
                .setSpring(
                    SpringForce()
                        .setStiffness(SPRING_STIFFNESS)
                        .setDampingRatio(SPRING_DAMPING_RATIO)
                )
                .setMinimumVisibleChange(ANIMATION_MINIMUM_VISIBLE_CHANGE)
                .addUpdateListener { _, value, _ -> view.applyAnimationProgress(value) }
        var junkListener: DynamicAnimation.OnAnimationUpdateListener? = null

        visibilityModel
            .mapLatest {
                when (it) {
                    is VolumeDialogVisibilityModel.Visible -> {
                        tracer.traceVisibilityEnd(it)
                        view.animateShow(
                            duration = dialogShowAnimationDurationMs,
                            translationX = calculateTranslationX(view),
                        )
                        junkListener?.let(animation::removeUpdateListener)
                        junkListener =
                            jankListenerFactory.show(view).also(animation::addUpdateListener)
                        animation.suspendAnimate(FRACTION_SHOW)
                    }
                    is VolumeDialogVisibilityModel.Dismissed -> {
                        tracer.traceVisibilityEnd(it)
                        view.animateHide(
                            duration = dialogHideAnimationDurationMs,
                            translationX = calculateTranslationX(view),
                        )
                        junkListener?.let(animation::removeUpdateListener)
                        junkListener =
                            jankListenerFactory.dismiss(view).also(animation::addUpdateListener)
                        animation.suspendAnimate(FRACTION_HIDE)
                        dialog.dismiss()
                    }
                    is VolumeDialogVisibilityModel.Invisible -> {
@@ -136,37 +153,21 @@ constructor(
            .launchIn(this)
    }

    private fun calculateTranslationX(view: View): Float? {
        return if (view.display.rotation == RotationPolicy.NATURAL_ROTATION) {
            if (view.isLayoutRtl) {
    /**
     * @param fraction in range [0, 1]. 0 corresponds to the dialog being hidden and 1 - visible.
     */
    private fun View.applyAnimationProgress(fraction: Float) {
        alpha = ceil(fraction)
        if (display.rotation == RotationPolicy.NATURAL_ROTATION) {
                if (isLayoutRtl) {
                    -1
                } else {
                    1
            } * view.width / 2f
                } * width / 2f
            } else {
                null
            }
    }

    private suspend fun View.animateShow(duration: Long, translationX: Float?) {
        translationX?.let { setTranslationX(translationX) }
        alpha = 0f
        animate()
            .alpha(1f)
            .translationX(0f)
            .setDuration(duration)
            .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator())
            .suspendAnimate(jankListenerFactory.show(this, duration))
    }

    private suspend fun View.animateHide(duration: Long, translationX: Float?) {
        val animator =
            animate()
                .alpha(0f)
                .setDuration(duration)
                .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator())
        translationX?.let { animator.translationX(it) }
        animator.suspendAnimate(jankListenerFactory.dismiss(this, duration))
            ?.let { maxTranslationX -> translationX = lerp(maxTranslationX, 0f, fraction) }
    }

    private suspend fun ViewTreeObserver.listenToComputeInternalInsets() =
+20 −19
Original line number Diff line number Diff line
@@ -17,8 +17,8 @@
package com.android.systemui.volume.dialog.ui.utils

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
@@ -30,35 +30,36 @@ class JankListenerFactory
@Inject
constructor(private val interactionJankMonitor: InteractionJankMonitor) {

    fun show(view: View, timeout: Long) = getJunkListener(view, "show", timeout)

    fun update(view: View, timeout: Long) = getJunkListener(view, "update", timeout)
    fun show(view: View): DynamicAnimation.OnAnimationUpdateListener {
        return createJunkListener(view, "show")
    }

    fun dismiss(view: View, timeout: Long) = getJunkListener(view, "dismiss", timeout)
    fun dismiss(view: View): DynamicAnimation.OnAnimationUpdateListener {
        return createJunkListener(view, "dismiss")
    }

    private fun getJunkListener(
    private fun createJunkListener(
        view: View,
        type: String,
        timeout: Long,
    ): Animator.AnimatorListener {
        return object : AnimatorListenerAdapter() {
            override fun onAnimationStart(animation: Animator) {
    ): DynamicAnimation.OnAnimationUpdateListener {
        var trackedStart = false
        return DynamicAnimation.OnAnimationUpdateListener { animation, _, _ ->
            if (!trackedStart) {
                trackedStart = true
                interactionJankMonitor.begin(
                    InteractionJankMonitor.Configuration.Builder.withView(
                            Cuj.CUJ_VOLUME_CONTROL,
                            view,
                        )
                        .setTag(type)
                        .setTimeout(timeout)
                )
            }

            override fun onAnimationEnd(animation: Animator) {
                animation.addEndListener { _, canceled, _, _ ->
                    if (canceled) {
                        interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL)
                    } else {
                        interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL)
                    }

            override fun onAnimationCancel(animation: Animator) {
                interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL)
                }
            }
        }
    }
+16 −14
Original line number Diff line number Diff line
@@ -96,15 +96,17 @@ suspend fun <T> ValueAnimator.suspendAnimate(onValueChanged: (T) -> Unit) {
 * Starts spring animation and suspends until it's finished. Cancels the animation if the running
 * coroutine is cancelled.
 */
suspend fun SpringAnimation.suspendAnimate(onAnimationUpdate: (Float) -> Unit) =
    suspendCancellableCoroutine { continuation ->
suspend fun SpringAnimation.suspendAnimate(
    finalPosition: Float = 1f,
    onAnimationUpdate: (Float) -> Unit = {},
) = suspendCancellableCoroutine { continuation ->
    val updateListener =
        DynamicAnimation.OnAnimationUpdateListener { _, value, _ -> onAnimationUpdate(value) }
    val endListener =
        DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> continuation.resumeIfCan(Unit) }
    addUpdateListener(updateListener)
    addEndListener(endListener)
        animateToFinalPosition(1F)
    animateToFinalPosition(finalPosition)
    continuation.invokeOnCancellation {
        removeUpdateListener(updateListener)
        removeEndListener(endListener)
+0 −2
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.volume.dialog.ui.binder

import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder
import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder
@@ -28,7 +27,6 @@ import com.android.systemui.volume.dialog.utils.volumeTracer
val Kosmos.volumeDialogViewBinder by
    Kosmos.Fixture {
        VolumeDialogViewBinder(
            applicationContext.resources,
            volumeDialogViewModel,
            jankListenerFactory,
            volumeTracer,