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

Commit ad58b2d2 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Making haptics in new volume dialog slider continuous.

Given the short length of the slider, dragging through its many discrete
steps makes it feel like the slider is actually continuous. Therefore,
we make sure we deliver continous haptics on the vertical volume slider.
In the case of the horizontal sliders in the volume panel, the surface
area is large enough so that the discrete steps are salient in the
dragging motion. Thus, we keep these haptics discrete.

We achieve these behaviors by providing different bundles of
SliderHapticFeedbackConfig and SeekableSliderTrackerConfig objects in a
new VolumeHapticsConfigs data class. Discretized configs are supplied to
the volume sliders in the volume panel, and continuous configs are given
to the vertical volume dialog.

Test: manual. Verified that the volume dialog delivers continuous
  haptics and the volume panel delivers discrete haptics.
Flag: EXEMPT bugfix
Bug: 407025177

Change-Id: I6e9d16f04060af68081f4728a018c862c34dce19
parent a453f06d
Loading
Loading
Loading
Loading
+11 −6
Original line number Diff line number Diff line
@@ -192,7 +192,11 @@ fun VolumeSlider(
                        hapticsViewModelFactory?.let {
                            Haptics.Enabled(
                                hapticsViewModelFactory = it,
                                hapticFilter = state.hapticFilter,
                                hapticConfigs =
                                    VolumeHapticsConfigsProvider.discreteConfigs(
                                        state.valueRange.stepSize(),
                                        state.hapticFilter,
                                    ),
                                orientation = Orientation.Horizontal,
                            )
                        } ?: Haptics.Disabled,
@@ -372,16 +376,15 @@ private fun setUpHapticsViewModel(
    hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
): SliderHapticsViewModel? {
    return hapticsViewModelFactory?.let {
        val configs =
            VolumeHapticsConfigsProvider.discreteConfigs(valueRange.stepSize(), hapticFilter)
        rememberViewModel(traceName = "SliderHapticsViewModel") {
                it.create(
                    interactionSource,
                    valueRange,
                    Orientation.Horizontal,
                    VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
                        valueRange,
                        hapticFilter,
                    ),
                    VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
                    configs.hapticFeedbackConfig,
                    configs.sliderTrackerConfig,
                )
            }
            .also { hapticsViewModel ->
@@ -399,3 +402,5 @@ private fun setUpHapticsViewModel(
            }
    }
}

private fun ClosedFloatingPointRange<Float>.stepSize(): Float = 1f / (endInclusive - start)
+5 −1
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.ui.compose.slider.AccessibilityParams
import com.android.systemui.volume.ui.compose.slider.Haptics
import com.android.systemui.volume.ui.compose.slider.Slider
@@ -130,7 +131,10 @@ private fun VolumeDialogSlider(
            hapticsViewModelFactory?.let {
                Haptics.Enabled(
                    hapticsViewModelFactory = it,
                    hapticFilter = SliderHapticFeedbackFilter(),
                    hapticConfigs =
                        VolumeHapticsConfigsProvider.continuousConfigs(
                            SliderHapticFeedbackFilter()
                        ),
                    orientation = Orientation.Vertical,
                )
            } ?: Haptics.Disabled,
+50 −18
Original line number Diff line number Diff line
@@ -22,23 +22,55 @@ import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter

object VolumeHapticsConfigsProvider {

    fun sliderHapticFeedbackConfig(
        valueRange: ClosedFloatingPointRange<Float>,
        filter: SliderHapticFeedbackFilter = SliderHapticFeedbackFilter(),
    ): SliderHapticFeedbackConfig {
        val sliderStepSize = 1f / (valueRange.endInclusive - valueRange.start)
        return SliderHapticFeedbackConfig(
    fun discreteConfigs(stepSize: Float, filter: SliderHapticFeedbackFilter): VolumeHapticsConfigs =
        provideConfigs(stepSize, filter)

    fun continuousConfigs(filter: SliderHapticFeedbackFilter): VolumeHapticsConfigs =
        provideConfigs(stepSize = 0f, filter)

    private fun provideConfigs(
        stepSize: Float,
        filter: SliderHapticFeedbackFilter,
    ): VolumeHapticsConfigs {
        val hapticFeedbackConfig: SliderHapticFeedbackConfig
        val trackerConfig: SeekableSliderTrackerConfig
        if (stepSize == 0f) {
            // Create a set of continuous configs
            hapticFeedbackConfig =
                SliderHapticFeedbackConfig(
                    additionalVelocityMaxBump = 0.1f,
                    deltaProgressForDragThreshold = 0.05f,
                    numberOfLowTicks = 4,
                    maxVelocityToScale = 0.5f, /* slider progress(from 0 to 1) per sec */
                    filter = filter,
                )
            trackerConfig =
                SeekableSliderTrackerConfig(
                    lowerBookendThreshold = 0.01f,
                    upperBookendThreshold = 0.99f,
                )
        } else {
            // Create a set of discrete configs
            hapticFeedbackConfig =
                SliderHapticFeedbackConfig(
                    lowerBookendScale = 0.2f,
                    progressBasedDragMinScale = 0.2f,
                    progressBasedDragMaxScale = 0.5f,
                    deltaProgressForDragThreshold = 0f,
                    additionalVelocityMaxBump = 0.2f,
                    maxVelocityToScale = 0.1f, /* slider progress(from 0 to 1) per sec */
            sliderStepSize = sliderStepSize,
                    sliderStepSize = stepSize,
                    filter = filter,
                )
    }

    val seekableSliderTrackerConfig =
            trackerConfig =
                SeekableSliderTrackerConfig(lowerBookendThreshold = 0f, upperBookendThreshold = 1f)
        }
        return VolumeHapticsConfigs(hapticFeedbackConfig, trackerConfig)
    }
}

// A collection of configuration parameters for the haptics in the slider
data class VolumeHapticsConfigs(
    val hapticFeedbackConfig: SliderHapticFeedbackConfig,
    val sliderTrackerConfig: SeekableSliderTrackerConfig,
)
+15 −21
Original line number Diff line number Diff line
@@ -36,7 +36,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
@@ -46,14 +45,10 @@ import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.semantics.stateDescription
import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigs
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

@Composable
fun Slider(
@@ -95,7 +90,7 @@ fun Slider(
    val sliderState =
        remember(valueRange) { SliderState(value = animatedValue, valueRange = valueRange) }
    val valueChange: (Float) -> Unit = { newValue ->
        hapticsViewModel?.onValueChange(newValue)
        hapticsViewModel?.addVelocityDataPoint(newValue)
        onValueChanged(newValue)
    }
    val semantics =
@@ -193,23 +188,22 @@ private fun Haptics.createViewModel(
                            interactionSource,
                            valueRange,
                            orientation,
                            VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
                                valueRange,
                                hapticFilter,
                            ),
                            VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
                            hapticConfigs.hapticFeedbackConfig,
                            hapticConfigs.sliderTrackerConfig,
                        )
                    }
                    .also { hapticsViewModel ->
                        var lastDiscreteStep by remember { mutableFloatStateOf(value) }
                        var lastValue by remember { mutableFloatStateOf(value) }
                        LaunchedEffect(value) {
                            snapshotFlow { value }
                                .map { round(it) }
                                .filter { it != lastDiscreteStep }
                                .distinctUntilChanged()
                                .collect { discreteStep ->
                                    lastDiscreteStep = discreteStep
                                    hapticsViewModel.onValueChange(discreteStep)
                            val roundedValue =
                                if (hapticConfigs.hapticFeedbackConfig.sliderStepSize != 0f) {
                                    round(value)
                                } else {
                                    value
                                }
                            if (roundedValue != lastValue) {
                                lastValue = roundedValue
                                hapticsViewModel.onValueChange(roundedValue)
                            }
                        }
                    }
@@ -228,7 +222,7 @@ sealed interface Haptics {

    data class Enabled(
        val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
        val hapticFilter: SliderHapticFeedbackFilter,
        val hapticConfigs: VolumeHapticsConfigs,
        val orientation: Orientation,
    ) : Haptics
}