summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Juan Sebastian Martinez <juansmartinez@google.com> 2024-10-15 16:15:40 -0700
committer Juan Sebastian Martinez <juansmartinez@google.com> 2024-10-28 09:40:13 -0700
commitfb9b9295ccc5a8d87509091dc896871a68a89a6a (patch)
treecb9ec73dbeb6bb4fa1fd81d6536ba2bbb460f283
parentf6183631a950e2f760c7cc2773dbe201fc358afc (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
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt1
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt64
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelTest.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/compose/ui/SliderHapticsViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModelKosmos.kt4
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,
)
}
}