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

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

Wire haptics view model to the compose slider in the Volume Dialog

Flag: com.android.systemui.volume_redesign
Bug: 369994956
Test: atest VolumeDialogScreenshotTest
Test: manual on the phone. Check slider haptics works
Change-Id: I714601ab38905320ca37917bfc5192ed4cae1379
parent 26fdffbb
Loading
Loading
Loading
Loading
+47 −38
Original line number Diff line number Diff line
@@ -67,11 +67,10 @@ import com.android.systemui.Flags
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -103,6 +102,10 @@ fun VolumeSlider(
    }

    val value by valueState(state)
    val interactionSource = remember { MutableInteractionSource() }
    val hapticsViewModel: SliderHapticsViewModel? =
        setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory)

    Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -127,8 +130,14 @@ fun VolumeSlider(
        Slider(
            value = value,
            valueRange = state.valueRange,
            onValueChange = onValueChange,
            onValueChangeFinished = onValueChangeFinished,
            onValueChange = { newValue ->
                hapticsViewModel?.addVelocityDataPoint(newValue)
                onValueChange(newValue)
            },
            onValueChangeFinished = {
                hapticsViewModel?.onValueChangeEnded()
                onValueChangeFinished?.invoke()
            },
            enabled = state.isEnabled,
            modifier =
                Modifier.height(40.dp)
@@ -210,41 +219,8 @@ private fun LegacyVolumeSlider(
) {
    val value by valueState(state)
    val interactionSource = remember { MutableInteractionSource() }
    val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start)
    val hapticsViewModel: SliderHapticsViewModel? =
        hapticsViewModelFactory?.let {
            rememberViewModel(traceName = "SliderHapticsViewModel") {
                it.create(
                    interactionSource,
                    state.valueRange,
                    Orientation.Horizontal,
                    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,
                    ),
                    SeekableSliderTrackerConfig(
                        lowerBookendThreshold = 0f,
                        upperBookendThreshold = 1f,
                    ),
                )
            }
        }
    var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) }
    LaunchedEffect(value) {
        snapshotFlow { value }
            .map { round(it) }
            .filter { it != lastDiscreteStep }
            .distinctUntilChanged()
            .collect { discreteStep ->
                lastDiscreteStep = discreteStep
                hapticsViewModel?.onValueChange(discreteStep)
            }
    }
        setUpHapticsViewModel(value, state.valueRange, interactionSource, hapticsViewModelFactory)

    PlatformSlider(
        modifier =
@@ -357,3 +333,36 @@ private fun SliderIcon(
        content = { Icon(modifier = Modifier.size(24.dp), icon = icon) },
    )
}

@Composable
fun setUpHapticsViewModel(
    value: Float,
    valueRange: ClosedFloatingPointRange<Float>,
    interactionSource: MutableInteractionSource,
    hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
): SliderHapticsViewModel? {
    return hapticsViewModelFactory?.let {
        rememberViewModel(traceName = "SliderHapticsViewModel") {
                it.create(
                    interactionSource,
                    valueRange,
                    Orientation.Horizontal,
                    VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(valueRange),
                    VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
                )
            }
            .also { hapticsViewModel ->
                var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) }
                LaunchedEffect(value) {
                    snapshotFlow { value }
                        .map { round(it) }
                        .filter { it != lastDiscreteStep }
                        .distinctUntilChanged()
                        .collect { discreteStep ->
                            lastDiscreteStep = discreteStep
                            hapticsViewModel.onValueChange(discreteStep)
                        }
                }
            }
    }
}
+44 −3
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -36,7 +38,10 @@ import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEvent
@@ -46,14 +51,20 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.ui.graphics.painter.DrawablePainter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import javax.inject.Inject
import kotlin.math.round
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

@VolumeDialogSliderScope
class VolumeDialogSliderViewBinder
@@ -91,7 +102,7 @@ private fun VolumeDialogSlider(
    hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
    modifier: Modifier = Modifier,
) {
    // TODO (apotapov) use hapticsViewModelFactory

    val colors =
        SliderDefaults.colors(
            thumbColor = MaterialTheme.colorScheme.primary,
@@ -103,6 +114,20 @@ private fun VolumeDialogSlider(
    val collectedSliderState by viewModel.state.collectAsStateWithLifecycle(null)
    val sliderState = collectedSliderState ?: return

    val interactionSource = remember { MutableInteractionSource() }
    val hapticsViewModel: SliderHapticsViewModel? =
        hapticsViewModelFactory?.let {
            rememberViewModel(traceName = "SliderHapticsViewModel") {
                it.create(
                    interactionSource,
                    sliderState.valueRange,
                    Orientation.Vertical,
                    VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(sliderState.valueRange),
                    VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
                )
            }
        }

    val state =
        remember(sliderState.valueRange) {
            SliderState(
@@ -113,9 +138,13 @@ private fun VolumeDialogSlider(
                            .toInt(),
                )
                .apply {
                    onValueChangeFinished = { viewModel.onStreamChangeFinished(value.roundToInt()) }
                    onValueChangeFinished = {
                        viewModel.onStreamChangeFinished(value.roundToInt())
                        hapticsViewModel?.onValueChangeEnded()
                    }
                    setOnValueChangeListener {
                        value = it
                        hapticsViewModel?.addVelocityDataPoint(it)
                        overscrollViewModel.setSlider(
                            value = value,
                            min = valueRange.start,
@@ -125,13 +154,25 @@ private fun VolumeDialogSlider(
                    }
                }
        }
    LaunchedEffect(sliderState.value) { state.value = sliderState.value }
    var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderState.value)) }
    LaunchedEffect(sliderState.value) {
        state.value = sliderState.value
        snapshotFlow { sliderState.value }
            .map { round(it) }
            .filter { it != lastDiscreteStep }
            .distinctUntilChanged()
            .collect { discreteStep ->
                lastDiscreteStep = discreteStep
                hapticsViewModel?.onValueChange(discreteStep)
            }
    }

    VerticalSlider(
        state = state,
        enabled = !sliderState.isDisabled,
        reverseDirection = true,
        colors = colors,
        interactionSource = interactionSource,
        modifier =
            modifier.pointerInput(Unit) {
                awaitPointerEventScope {
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.volume.haptics.ui

import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig

object VolumeHapticsConfigsProvider {

    fun sliderHapticFeedbackConfig(
        valueRange: ClosedFloatingPointRange<Float>
    ): SliderHapticFeedbackConfig {
        val sliderStepSize = 1f / (valueRange.endInclusive - valueRange.start)
        return 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,
        )
    }

    val seekableSliderTrackerConfig =
        SeekableSliderTrackerConfig(lowerBookendThreshold = 0f, upperBookendThreshold = 1f)
}