diff options
| author | 2024-10-15 16:15:40 -0700 | |
|---|---|---|
| committer | 2024-10-28 09:40:13 -0700 | |
| commit | fb9b9295ccc5a8d87509091dc896871a68a89a6a (patch) | |
| tree | cb9ec73dbeb6bb4fa1fd81d6536ba2bbb460f283 | |
| parent | f6183631a950e2f760c7cc2773dbe201fc358afc (diff) | |
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
10 files changed, 86 insertions, 23 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index d4f3b5b6d6a6..28a12f813cf5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -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", ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt index d15430faa0a0..a0e46d51c73a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt @@ -49,6 +49,7 @@ fun GridVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, + hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index a23bb67215b5..eb79b906e5f8 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -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 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt index f80b36a10dc2..d3071f87f744 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt @@ -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, ) } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt index de242597f463..7fa83c64d5eb 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 2aa1ac99a400..39ccb870757d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -28,6 +28,7 @@ import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.common.shared.model.Icon +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons import com.android.systemui.res.R import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor @@ -61,6 +62,7 @@ constructor( private val zenModeInteractor: ZenModeInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, + override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 10714d1f41af..bb0dbaf01063 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.session.MediaController.PlaybackInfo import com.android.systemui.common.shared.model.Icon +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession @@ -40,6 +41,7 @@ constructor( @Assisted private val coroutineScope: CoroutineScope, private val context: Context, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, + override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { override val slider: StateFlow<SliderState> = diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt index 7ded8c5c9fc1..9c1783b99f78 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel +import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import kotlinx.coroutines.flow.StateFlow /** Controls the behaviour of a volume slider. */ @@ -23,6 +24,8 @@ interface SliderViewModel { val slider: StateFlow<SliderState> + val hapticsViewModelFactory: SliderHapticsViewModel.Factory + fun onValueChanged(state: SliderState, newValue: Float) fun onValueChangeFinished() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt index 55f0a28d0135..a78670d7f1cc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext import com.android.internal.logging.uiEventLogger +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.volume.domain.interactor.audioVolumeInteractor @@ -40,6 +41,7 @@ val Kosmos.audioStreamSliderViewModelFactory by zenModeInteractor, uiEventLogger, volumePanelLogger, + sliderHapticsViewModelFactory, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt index f0cb2cd904ca..abd4235143f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.applicationContext +import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession @@ -27,13 +28,14 @@ val Kosmos.castVolumeSliderViewModelFactory by object : CastVolumeSliderViewModel.Factory { override fun create( session: MediaDeviceSession, - coroutineScope: CoroutineScope + coroutineScope: CoroutineScope, ): CastVolumeSliderViewModel { return CastVolumeSliderViewModel( session, coroutineScope, applicationContext, mediaDeviceSessionInteractor, + sliderHapticsViewModelFactory, ) } } |