Loading packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +48 −47 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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()) Loading Loading @@ -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 -> { Loading @@ -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() = Loading packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt +20 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } } } } Loading packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +16 −14 Original line number Diff line number Diff line Loading @@ -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) Loading packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt +0 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -28,7 +27,6 @@ import com.android.systemui.volume.dialog.utils.volumeTracer val Kosmos.volumeDialogViewBinder by Kosmos.Fixture { VolumeDialogViewBinder( applicationContext.resources, volumeDialogViewModel, jankListenerFactory, volumeTracer, Loading Loading
packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +48 −47 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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()) Loading Loading @@ -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 -> { Loading @@ -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() = Loading
packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt +20 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } } } } Loading
packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +16 −14 Original line number Diff line number Diff line Loading @@ -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) Loading
packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt +0 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -28,7 +27,6 @@ import com.android.systemui.volume.dialog.utils.volumeTracer val Kosmos.volumeDialogViewBinder by Kosmos.Fixture { VolumeDialogViewBinder( applicationContext.resources, volumeDialogViewModel, jankListenerFactory, volumeTracer, Loading