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

Commit d7cf20f7 authored by Anton Potapov's avatar Anton Potapov
Browse files

Simplify slider animation code and reduce spring stiffness

That code was there to address that the Volume Dialog and Volume Slider
handle immediate volume state differently. Volume Dialog used to lack
any immediate state and relied on the Slider to hold it for a smoother
touch experience. Not it has this state in the View Model which allows
me to simplify the Composable and make a smoother animation when the
user holds a physical button

Flag: com.android.systemui.volume_redesign
Fixes: 398303967
Test: atest VolumeDialogScreenshotTest
Test: atest VolumePanelScreenshotTest
Test: manual on the foldable. Adjust volume with touch and physical
buttons

Change-Id: I80957ed57b08096c0be40f63c19d79be6246243d
parent 1760acbd
Loading
Loading
Loading
Loading
+28 −47
Original line number Original line Diff line number Diff line
@@ -18,9 +18,9 @@


package com.android.systemui.volume.ui.compose.slider
package com.android.systemui.volume.ui.compose.slider


import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -32,11 +32,11 @@ import androidx.compose.material3.SliderState
import androidx.compose.material3.VerticalSlider
import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
@@ -53,14 +53,9 @@ import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import kotlin.math.round
import kotlin.math.round
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

private val defaultSpring =
    SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)


@Composable
@Composable
fun Slider(
fun Slider(
@@ -87,55 +82,31 @@ fun Slider(
    },
    },
) {
) {
    require(stepDistance >= 0) { "stepDistance must not be negative" }
    require(stepDistance >= 0) { "stepDistance must not be negative" }
    val coroutineScope = rememberCoroutineScope()
    val snappedValue by valueState(value, isEnabled)
    val snappedValue = snapValue(value, valueRange, stepDistance)
    val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
    val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)


    val animatable = remember { Animatable(snappedValue) }
    var animationJob: Job? by remember { mutableStateOf(null) }
    val sliderState =
    val sliderState =
        remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
        remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
    val valueChange: (Float) -> Unit = { newValue ->
    val valueChange: (Float) -> Unit = { newValue ->
        hapticsViewModel?.onValueChange(newValue)
        hapticsViewModel?.onValueChange(newValue)
        val snappedNewValue = snapValue(newValue, valueRange, stepDistance)
        onValueChanged(newValue)
        if (animatable.targetValue != snappedNewValue) {
            onValueChanged(snappedNewValue)
            animationJob?.cancel()
            animationJob =
                coroutineScope.launch {
                    animatable.animateTo(
                        targetValue = snappedNewValue,
                        animationSpec = defaultSpring,
                    )
                }
        }
    }
    }
    val semantics =
    val semantics =
        createSemantics(
        createSemantics(
            accessibilityParams,
            accessibilityParams,
            animatable.targetValue,
            snappedValue,
            valueRange,
            valueRange,
            valueChange,
            valueChange,
            isEnabled,
            isEnabled,
            stepDistance,
            stepDistance,
        )
        )


    LaunchedEffect(snappedValue) {
        if (!animatable.isRunning && animatable.targetValue != snappedValue) {
            animationJob?.cancel()
            animationJob =
                coroutineScope.launch {
                    animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring)
                }
        }
    }

    sliderState.onValueChangeFinished = {
    sliderState.onValueChangeFinished = {
        hapticsViewModel?.onValueChangeEnded()
        hapticsViewModel?.onValueChangeEnded()
        onValueChangeFinished?.invoke(animatable.targetValue)
        onValueChangeFinished?.invoke(snappedValue)
    }
    }
    sliderState.onValueChange = valueChange
    sliderState.onValueChange = valueChange
    sliderState.value = animatable.value
    sliderState.value = snappedValue


    if (isVertical) {
    if (isVertical) {
        VerticalSlider(
        VerticalSlider(
@@ -161,17 +132,27 @@ fun Slider(
    }
    }
}
}


private fun snapValue(
@Composable
    value: Float,
private fun valueState(targetValue: Float, isEnabled: Boolean): State<Float> {
    valueRange: ClosedFloatingPointRange<Float>,
    var prevValue by remember { mutableFloatStateOf(targetValue) }
    stepDistance: Float,
    var prevEnabled by remember { mutableStateOf(isEnabled) }
): Float {
    // Don't animate slider value when receive the first value and when changing isEnabled state
    if (stepDistance == 0f) {
    val value =
        if (prevEnabled != isEnabled) mutableFloatStateOf(targetValue)
        else
            animateFloatAsState(
                targetValue = targetValue,
                animationSpec =
                    spring(
                        dampingRatio = Spring.DampingRatioNoBouncy,
                        stiffness = Spring.StiffnessMedium,
                    ),
                label = "VolumeSliderValueAnimation",
            )
    prevValue = targetValue
    prevEnabled = isEnabled
    return value
    return value
}
}
    val coercedValue = value.coerceIn(valueRange)
    return Math.round(coercedValue / stepDistance) * stepDistance
}


private fun createSemantics(
private fun createSemantics(
    params: AccessibilityParams,
    params: AccessibilityParams,