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

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

Adding slider haptics to volume sliders in the volume panel.

Test: manual. Verified haptics are delivered on discrete steps of all
  sliders in the volume panel
Flag: com.android.systemui.haptics_for_compose_sliders
Bug: 373919020
Bug: 341968766
Change-Id: Ib409146c2ee119bfb862e53ba589a8c7547d8610
parent f6183631
Loading
Loading
Loading
Loading
+6 −8
Original line number Diff line number Diff line
@@ -78,9 +78,7 @@ fun ColumnVolumeSliders(
) {
    require(viewModels.isNotEmpty())
    Column(modifier = modifier) {
        Box(
            modifier = Modifier.fillMaxWidth(),
        ) {
        Box(modifier = Modifier.fillMaxWidth()) {
            val sliderViewModel: SliderViewModel = viewModels.first()
            val sliderState by viewModels.first().slider.collectAsStateWithLifecycle()
            val sliderPadding by topSliderPadding(isExpandable)
@@ -94,6 +92,7 @@ fun ColumnVolumeSliders(
                onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                sliderColors = sliderColors,
                hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
            )

            ExpandButton(
@@ -143,6 +142,7 @@ fun ColumnVolumeSliders(
                            onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                            onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                            sliderColors = sliderColors,
                            hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
                        )
                    }
                }
@@ -181,7 +181,7 @@ private fun ExpandButton(
            colors =
                IconButtonDefaults.filledIconButtonColors(
                    containerColor = sliderColors.indicatorColor,
                    contentColor = sliderColors.iconColor
                    contentColor = sliderColors.iconColor,
                ),
        ) {
            Icon(
@@ -211,9 +211,7 @@ private fun enterTransition(index: Int, totalCount: Int): EnterTransition {
            animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
            clip = false,
        ) +
        fadeIn(
            animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay),
        )
        fadeIn(animationSpec = tween(durationMillis = enterDuration, delayMillis = enterDelay))
}

private fun exitTransition(index: Int, totalCount: Int): ExitTransition {
@@ -286,6 +284,6 @@ private fun topSliderPadding(isExpandable: Boolean): State<Dp> {
                0.dp
            },
        animationSpec = animationSpec,
        label = "TopVolumeSliderPadding"
        label = "TopVolumeSliderPadding",
    )
}
+1 −0
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ fun GridVolumeSliders(
                onValueChangeFinished = { sliderViewModel.onValueChangeFinished() },
                onIconTapped = { sliderViewModel.toggleMuted(sliderState) },
                sliderColors = sliderColors,
                hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory,
            )
        }
    }
+53 −11
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
@@ -46,9 +48,14 @@ import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSlider
import com.android.compose.PlatformSliderColors
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.volume.panel.component.volume.slider.ui.viewmodel.SliderState

@Composable
@@ -59,8 +66,40 @@ fun VolumeSlider(
    onIconTapped: () -> Unit,
    modifier: Modifier = Modifier,
    sliderColors: PlatformSliderColors,
    hapticsViewModelFactory: SliderHapticsViewModel.Factory,
) {
    val value by valueState(state)
    val interactionSource = remember { MutableInteractionSource() }
    val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start)
    val hapticsViewModel: SliderHapticsViewModel? =
        if (Flags.hapticsForComposeSliders()) {
            rememberViewModel(traceName = "SliderHapticsViewModel") {
                hapticsViewModelFactory.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,
                    ),
                )
            }
        } else {
            null
        }

    // Perform haptics due to UI composition
    hapticsViewModel?.onValueChange(value)

    PlatformSlider(
        modifier =
            modifier.sysuiResTag(state.label).clearAndSetSemantics {
@@ -94,7 +133,7 @@ fun VolumeSlider(
                    val newValue =
                        (value + targetDirection * state.a11yStep).coerceIn(
                            state.valueRange.start,
                            state.valueRange.endInclusive
                            state.valueRange.endInclusive,
                        )
                    onValueChange(newValue)
                    true
@@ -102,16 +141,18 @@ fun VolumeSlider(
            },
        value = value,
        valueRange = state.valueRange,
        onValueChange = onValueChange,
        onValueChangeFinished = onValueChangeFinished,
        onValueChange = { newValue ->
            hapticsViewModel?.addVelocityDataPoint(newValue)
            onValueChange(newValue)
        },
        onValueChangeFinished = {
            hapticsViewModel?.onValueChangeEnded()
            onValueChangeFinished?.invoke()
        },
        enabled = state.isEnabled,
        icon = {
            state.icon?.let {
                SliderIcon(
                    icon = it,
                    onIconTapped = onIconTapped,
                    isTappable = state.isMutable,
                )
                SliderIcon(icon = it, onIconTapped = onIconTapped, isTappable = state.isMutable)
            }
        },
        colors = sliderColors,
@@ -128,7 +169,8 @@ fun VolumeSlider(
                    disabledMessage = state.disabledMessage,
                )
            }
        }
        },
        interactionSource = interactionSource,
    )
}

@@ -150,14 +192,14 @@ private fun SliderIcon(
    icon: Icon,
    onIconTapped: () -> Unit,
    isTappable: Boolean,
    modifier: Modifier = Modifier
    modifier: Modifier = Modifier,
) {
    val boxModifier =
        if (isTappable) {
                modifier.clickable(
                    onClick = onIconTapped,
                    interactionSource = null,
                    indication = null
                    indication = null,
                )
            } else {
                modifier
+2 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.AudioStream
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -73,6 +74,7 @@ class AudioStreamSliderViewModelTest : SysuiTestCase() {
            kosmos.zenModeInteractor,
            kosmos.uiEventLogger,
            kosmos.volumePanelLogger,
            kosmos.sliderHapticsViewModelFactory,
        )
    }

+12 −3
Original line number Diff line number Diff line
@@ -143,18 +143,20 @@ constructor(
            SliderEventType.STARTED_TRACKING_TOUCH -> {
                startingProgress = normalized
                currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER
                sliderStateProducer.onProgressChanged(true, normalized)
            }
            SliderEventType.PROGRESS_CHANGE_BY_USER -> {
                velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset())
                addVelocityDataPoint(value)
                currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_USER
                sliderStateProducer.onProgressChanged(true, normalized)
            }
            SliderEventType.STARTED_TRACKING_PROGRAM -> {
                startingProgress = normalized
                currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
                sliderStateProducer.onProgressChanged(false, normalized)
            }
            SliderEventType.PROGRESS_CHANGE_BY_PROGRAM -> {
                velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset())
                addVelocityDataPoint(value)
                currentSliderEventType = SliderEventType.PROGRESS_CHANGE_BY_PROGRAM
                sliderStateProducer.onProgressChanged(false, normalized)
            }
@@ -162,6 +164,11 @@ constructor(
        }
    }

    fun addVelocityDataPoint(value: Float) {
        val normalized = value.normalize()
        velocityTracker.addPosition(System.currentTimeMillis(), normalized.toOffset())
    }

    fun onValueChangeEnded() {
        when (currentSliderEventType) {
            SliderEventType.STARTED_TRACKING_PROGRAM,
@@ -174,8 +181,10 @@ constructor(
        velocityTracker.resetTracking()
    }

    private fun ClosedFloatingPointRange<Float>.length(): Float = endInclusive - start

    private fun Float.normalize(): Float =
        (this / (sliderRange.endInclusive - sliderRange.start)).coerceIn(0f, 1f)
        ((this - sliderRange.start) / sliderRange.length()).coerceIn(0f, 1f)

    private fun Float.toOffset(): Offset =
        when (orientation) {
Loading