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

Commit 6a238005 authored by Anton Potapov's avatar Anton Potapov
Browse files

Rework Volume Dialog appear animation using DynamicAnimation

Flag: com.android.systemui.volume_redesign
Fixes: 395819674
Test: manual on the phone. Observe Volume Dialog show and hide
animations

Change-Id: I70d468632ccd0b86c4cbd0389f969116585c479b
parent ca75ea98
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,