summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Anton Potapov <apotapov@google.com> 2025-01-24 03:03:28 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-01-24 03:03:28 -0800
commitd5f140b0e9833ec92d8d21fa302d0cd5a76aa94d (patch)
tree7a24c3bd1a7770f668375eb3f34f2b7e3140916a
parenta5d2a6afada1773a4abbf1bfddfb82424daba877 (diff)
parenta45edcfc7586d2cac79f83280dea7ae775671969 (diff)
Merge changes I714601ab,I2f100f41,I8ded71ea into main
* changes: Wire haptics view model to the compose slider in the Volume Dialog Rework VolumeDialog slider using Compose Add VolumeDialogSliderTrack to support animated icons at the end of the track.
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt85
-rw-r--r--packages/SystemUI/res/layout/volume_dialog_slider.xml8
-rw-r--r--packages/SystemUI/res/values/styles.xml10
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt82
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt253
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt347
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt41
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt33
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt4
16 files changed, 654 insertions, 264 deletions
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 bdd0da9ce4a4..4e10ff689b19 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
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
index c5f468e731f5..2628f4991b49 100644
--- a/packages/SystemUI/res/layout/volume_dialog_slider.xml
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -18,14 +18,10 @@
android:layout_height="match_parent"
android:maxHeight="@dimen/volume_dialog_slider_height">
- <com.google.android.material.slider.Slider
+ <androidx.compose.ui.platform.ComposeView
android:id="@+id/volume_dialog_slider"
- style="@style/SystemUI.Material3.Slider.Volume"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
- android:layout_marginTop="-20dp"
- android:layout_marginBottom="-20dp"
- android:orientation="vertical"
- android:theme="@style/Theme.Material3.DayNight" />
+ android:orientation="vertical" />
</FrameLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index b0d9bed05e27..c95c6dd89353 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -565,16 +565,6 @@
<item name="android:windowNoTitle">true</item>
</style>
- <style name="SystemUI.Material3.Slider.Volume">
- <item name="trackHeight">40dp</item>
- <item name="thumbHeight">52dp</item>
- <item name="trackCornerSize">12dp</item>
- <item name="trackInsideCornerSize">2dp</item>
- <item name="trackStopIndicatorSize">6dp</item>
- <item name="trackIconSize">20dp</item>
- <item name="labelBehavior">gone</item>
- </style>
-
<style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider">
<item name="labelStyle">@style/Widget.Material3.Slider.Label</item>
<item name="thumbColor">@color/thumb_color</item>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
index 39b434ad65f1..83b7c1818341 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt
@@ -16,7 +16,6 @@
package com.android.systemui.volume.dialog
-import android.app.Dialog
import android.content.Context
import android.graphics.PixelFormat
import android.os.Bundle
@@ -24,6 +23,7 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
+import androidx.activity.ComponentDialog
import com.android.app.tracing.coroutines.coroutineScopeTraced
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -40,7 +40,7 @@ constructor(
@Application context: Context,
private val componentFactory: VolumeDialogComponent.Factory,
private val visibilityInteractor: VolumeDialogVisibilityInteractor,
-) : Dialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
+) : ComponentDialog(context, R.style.Theme_SystemUI_Dialog_Volume) {
init {
with(window!!) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
index 940c79c78d76..577e47bb3b83 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponent.kt
@@ -18,7 +18,6 @@ package com.android.systemui.volume.dialog.sliders.dagger
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
import dagger.BindsInstance
import dagger.Subcomponent
@@ -33,8 +32,6 @@ interface VolumeDialogSliderComponent {
fun sliderViewBinder(): VolumeDialogSliderViewBinder
- fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder
-
fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder
@Subcomponent.Factory
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
index 8109b50aa34a..38feb69aad7b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogOverscrollViewBinder.kt
@@ -20,11 +20,9 @@ import android.view.View
import androidx.dynamicanimation.animation.FloatValueHolder
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
-import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel.OverscrollEventModel
-import com.google.android.material.slider.Slider
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
@@ -51,10 +49,6 @@ constructor(private val viewModel: VolumeDialogOverscrollViewModel) {
)
.addUpdateListener { _, value, _ -> viewsToAnimate.setTranslationY(value) }
- view.requireViewById<Slider>(R.id.volume_dialog_slider).addOnChangeListener { s, value, _ ->
- viewModel.setSlider(value = value, min = s.valueFrom, max = s.valueTo)
- }
-
viewModel.overscrollEvent
.onEach { event ->
when (event) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt
deleted file mode 100644
index 5a7fbc6341f2..000000000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinder.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2024 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.dialog.sliders.ui
-
-import android.view.View
-import com.android.systemui.haptics.slider.HapticSlider
-import com.android.systemui.haptics.slider.HapticSliderPlugin
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.time.SystemClock
-import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
-import com.android.systemui.volume.dialog.sliders.shared.model.SliderInputEvent
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderInputEventsViewModel
-import com.google.android.material.slider.Slider
-import com.google.android.msdl.domain.MSDLPlayer
-import javax.inject.Inject
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-
-@VolumeDialogSliderScope
-class VolumeDialogSliderHapticsViewBinder
-@Inject
-constructor(
- private val inputEventsViewModel: VolumeDialogSliderInputEventsViewModel,
- private val vibratorHelper: VibratorHelper,
- private val msdlPlayer: MSDLPlayer,
- private val systemClock: SystemClock,
-) {
-
- fun CoroutineScope.bind(view: View) {
- val sliderView = view.requireViewById<Slider>(R.id.volume_dialog_slider)
- val hapticSliderPlugin =
- HapticSliderPlugin(
- slider = HapticSlider.Slider(sliderView),
- vibratorHelper = vibratorHelper,
- msdlPlayer = msdlPlayer,
- systemClock = systemClock,
- )
- hapticSliderPlugin.startInScope(this)
-
- sliderView.addOnChangeListener { _, value, fromUser ->
- hapticSliderPlugin.onProgressChanged(value.roundToInt(), fromUser)
- }
- sliderView.addOnSliderTouchListener(
- object : Slider.OnSliderTouchListener {
-
- override fun onStartTrackingTouch(slider: Slider) {
- hapticSliderPlugin.onStartTrackingTouch()
- }
-
- override fun onStopTrackingTouch(slider: Slider) {
- hapticSliderPlugin.onStopTrackingTouch()
- }
- }
- )
-
- inputEventsViewModel.event
- .onEach {
- when (it) {
- is SliderInputEvent.Button -> hapticSliderPlugin.onKeyDown()
- is SliderInputEvent.Touch -> hapticSliderPlugin.onTouchEvent(it.event)
- }
- }
- .launchIn(this)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index d40302408dd6..932f8aececa8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -16,96 +16,219 @@
package com.android.systemui.volume.dialog.sliders.ui
-import android.annotation.SuppressLint
+import android.graphics.drawable.Drawable
+import android.view.MotionEvent
import android.view.View
-import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.SpringAnimation
-import androidx.dynamicanimation.animation.SpringForce
+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
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+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
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.ComposeView
+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.VolumeDialogSliderStateModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.google.android.material.slider.Slider
-import com.google.android.material.slider.Slider.OnSliderTouchListener
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import javax.inject.Inject
+import kotlin.math.round
import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
@VolumeDialogSliderScope
class VolumeDialogSliderViewBinder
@Inject
constructor(
private val viewModel: VolumeDialogSliderViewModel,
+ private val overscrollViewModel: VolumeDialogOverscrollViewModel,
private val inputViewModel: VolumeDialogSliderInputEventsViewModel,
+ private val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
) {
+ fun bind(view: View) {
+ val sliderComposeView: ComposeView = view.requireViewById(R.id.volume_dialog_slider)
+ sliderComposeView.setContent {
+ VolumeDialogSlider(
+ viewModel = viewModel,
+ inputViewModel = inputViewModel,
+ overscrollViewModel = overscrollViewModel,
+ hapticsViewModelFactory =
+ if (com.android.systemui.Flags.hapticsForComposeSliders()) {
+ hapticsViewModelFactory
+ } else {
+ null
+ },
+ )
+ }
+ }
+}
- private val sliderValueProperty =
- object : FloatPropertyCompat<Slider>("value") {
- override fun getValue(slider: Slider): Float = slider.value
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun VolumeDialogSlider(
+ viewModel: VolumeDialogSliderViewModel,
+ inputViewModel: VolumeDialogSliderInputEventsViewModel,
+ overscrollViewModel: VolumeDialogOverscrollViewModel,
+ hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
+ modifier: Modifier = Modifier,
+) {
+
+ val colors =
+ SliderDefaults.colors(
+ thumbColor = MaterialTheme.colorScheme.primary,
+ activeTickColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ inactiveTickColor = MaterialTheme.colorScheme.primary,
+ activeTrackColor = MaterialTheme.colorScheme.primary,
+ inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ )
+ val collectedSliderState by viewModel.state.collectAsStateWithLifecycle(null)
+ val sliderState = collectedSliderState ?: return
- override fun setValue(slider: Slider, value: Float) {
- slider.value = value
+ 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,
+ )
}
}
- private val springForce =
- SpringForce().apply {
- stiffness = SpringForce.STIFFNESS_MEDIUM
- dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
- }
- @SuppressLint("ClickableViewAccessibility")
- fun CoroutineScope.bind(view: View) {
- var isInitialUpdate = true
- val sliderView: Slider = view.requireViewById(R.id.volume_dialog_slider)
- val animation = SpringAnimation(sliderView, sliderValueProperty)
- animation.spring = springForce
- sliderView.setOnTouchListener { _, event ->
- inputViewModel.onTouchEvent(event)
- false
- }
- sliderView.addOnChangeListener { _, value, fromUser ->
- viewModel.setStreamVolume(value.roundToInt(), fromUser)
+ val state =
+ remember(sliderState.valueRange) {
+ SliderState(
+ value = sliderState.value,
+ valueRange = sliderState.valueRange,
+ steps =
+ (sliderState.valueRange.endInclusive - sliderState.valueRange.start - 1)
+ .toInt(),
+ )
+ .apply {
+ onValueChangeFinished = {
+ viewModel.onStreamChangeFinished(value.roundToInt())
+ hapticsViewModel?.onValueChangeEnded()
+ }
+ setOnValueChangeListener {
+ value = it
+ hapticsViewModel?.addVelocityDataPoint(it)
+ overscrollViewModel.setSlider(
+ value = value,
+ min = valueRange.start,
+ max = valueRange.endInclusive,
+ )
+ viewModel.setStreamVolume(it, true)
+ }
+ }
}
- sliderView.addOnSliderTouchListener(
- object : OnSliderTouchListener {
- override fun onStartTrackingTouch(slider: Slider) {}
+ 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)
+ }
+ }
- override fun onStopTrackingTouch(slider: Slider) {
- viewModel.onStreamChangeFinished(slider.value.roundToInt())
+ VerticalSlider(
+ state = state,
+ enabled = !sliderState.isDisabled,
+ reverseDirection = true,
+ colors = colors,
+ interactionSource = interactionSource,
+ modifier =
+ modifier.pointerInput(Unit) {
+ awaitPointerEventScope {
+ // we should wait for all new pointer events
+ while (true) {
+ val event: PointerEvent = awaitPointerEvent()
+ PointerEvent::class
+ .java
+ .methods
+ .find { it.name.startsWith("getMotionEvent") }!!
+ .invoke(event)
+ ?.let { it as? MotionEvent? }
+ ?.let { inputViewModel.onTouchEvent(it) }
+ }
}
- }
- )
+ },
+ track = {
+ VolumeDialogSliderTrack(
+ state,
+ colors = colors,
+ isEnabled = !sliderState.isDisabled,
+ activeTrackEndIcon = { iconsState ->
+ VolumeIcon(sliderState.icon, iconsState.isActiveTrackEndIconVisible)
+ },
+ inactiveTrackEndIcon = { iconsState ->
+ VolumeIcon(sliderState.icon, !iconsState.isActiveTrackEndIconVisible)
+ },
+ )
+ },
+ )
+}
- viewModel.isDisabledByZenMode.onEach { sliderView.isEnabled = !it }.launchIn(this)
- viewModel.state
- .onEach {
- sliderView.setModel(it, animation, isInitialUpdate)
- isInitialUpdate = false
- }
- .launchIn(this)
+@Composable
+private fun BoxScope.VolumeIcon(
+ drawable: Drawable,
+ isVisible: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(animationSpec = tween(durationMillis = 50)),
+ exit = fadeOut(animationSpec = tween(durationMillis = 50)),
+ modifier = modifier.align(Alignment.Center).size(40.dp).padding(10.dp),
+ ) {
+ Icon(painter = DrawablePainter(drawable), contentDescription = null)
}
+}
- @SuppressLint("UseCompatLoadingForDrawables")
- private fun Slider.setModel(
- model: VolumeDialogSliderStateModel,
- animation: SpringAnimation,
- isInitialUpdate: Boolean,
- ) {
- valueFrom = model.minValue
- animation.setMinValue(model.minValue)
- valueTo = model.maxValue
- animation.setMaxValue(model.maxValue)
- // coerce the current value to the new value range before animating it. This prevents
- // animating from the value that is outside of current [valueFrom, valueTo].
- value = value.coerceIn(valueFrom, valueTo)
- trackIconActiveStart = model.icon
- if (isInitialUpdate) {
- value = model.value
- } else {
- animation.animateToFinalPosition(model.value)
- }
+@OptIn(ExperimentalMaterial3Api::class)
+fun SliderState.setOnValueChangeListener(onValueChange: ((Float) -> Unit)?) {
+ with(javaClass.getDeclaredField("onValueChange")) {
+ val oldIsAccessible = isAccessible
+ AutoCloseable { isAccessible = oldIsAccessible }
+ .use {
+ isAccessible = true
+ set(this@setOnValueChangeListener, onValueChange)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
index 75d427acc05b..c66955a0c187 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -71,7 +71,6 @@ constructor(private val viewModel: VolumeDialogSlidersViewModel) {
viewsToAnimate: Array<View>,
) {
with(component.sliderViewBinder()) { bind(sliderContainer) }
- with(component.sliderHapticsViewBinder()) { bind(sliderContainer) }
with(component.overscrollViewBinder()) { bind(sliderContainer, viewsToAnimate) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt
new file mode 100644
index 000000000000..1dd9ddac79be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt
@@ -0,0 +1,347 @@
+/*
+ * 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.dialog.sliders.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFilter
+import androidx.compose.ui.util.fastFirst
+import kotlin.math.min
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+fun VolumeDialogSliderTrack(
+ sliderState: SliderState,
+ colors: SliderColors,
+ isEnabled: Boolean,
+ modifier: Modifier = Modifier,
+ thumbTrackGapSize: Dp = 6.dp,
+ trackCornerSize: Dp = 12.dp,
+ trackInsideCornerSize: Dp = 2.dp,
+ trackSize: Dp = 40.dp,
+ activeTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+ inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null,
+) {
+ val measurePolicy = remember(sliderState) { TrackMeasurePolicy(sliderState) }
+ Layout(
+ measurePolicy = measurePolicy,
+ content = {
+ SliderDefaults.Track(
+ sliderState = sliderState,
+ colors = colors,
+ enabled = isEnabled,
+ trackCornerSize = trackCornerSize,
+ trackInsideCornerSize = trackInsideCornerSize,
+ drawStopIndicator = null,
+ thumbTrackGapSize = thumbTrackGapSize,
+ drawTick = { _, _ -> },
+ modifier = Modifier.width(trackSize).layoutId(Contents.Track),
+ )
+
+ TrackIcon(
+ icon = activeTrackStartIcon,
+ contentsId = Contents.Active.TrackStartIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = activeTrackEndIcon,
+ contentsId = Contents.Active.TrackEndIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = inactiveTrackStartIcon,
+ contentsId = Contents.Inactive.TrackStartIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ TrackIcon(
+ icon = inactiveTrackEndIcon,
+ contentsId = Contents.Inactive.TrackEndIcon,
+ isEnabled = isEnabled,
+ colors = colors,
+ state = measurePolicy,
+ )
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun TrackIcon(
+ icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?,
+ isEnabled: Boolean,
+ contentsId: Contents,
+ state: SliderIconsState,
+ colors: SliderColors,
+ modifier: Modifier = Modifier,
+) {
+ icon ?: return
+ Box(modifier = modifier.layoutId(contentsId).fillMaxSize()) {
+ CompositionLocalProvider(
+ LocalContentColor provides contentsId.getColor(colors, isEnabled)
+ ) {
+ icon(state)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private class TrackMeasurePolicy(private val sliderState: SliderState) :
+ MeasurePolicy, SliderIconsState {
+
+ private val isVisible: Map<Contents, MutableState<Boolean>> =
+ mutableMapOf(
+ Contents.Active.TrackStartIcon to mutableStateOf(false),
+ Contents.Active.TrackEndIcon to mutableStateOf(false),
+ Contents.Inactive.TrackStartIcon to mutableStateOf(false),
+ Contents.Inactive.TrackEndIcon to mutableStateOf(false),
+ )
+
+ override val isActiveTrackStartIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Active.TrackStartIcon).value
+
+ override val isActiveTrackEndIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Active.TrackEndIcon).value
+
+ override val isInactiveTrackStartIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Inactive.TrackStartIcon).value
+
+ override val isInactiveTrackEndIconVisible: Boolean
+ get() = isVisible.getValue(Contents.Inactive.TrackEndIcon).value
+
+ override fun MeasureScope.measure(
+ measurables: List<Measurable>,
+ constraints: Constraints,
+ ): MeasureResult {
+ val track = measurables.fastFirst { it.layoutId == Contents.Track }.measure(constraints)
+
+ val iconSize = min(track.width, track.height)
+ val iconConstraints = constraints.copy(maxWidth = iconSize, maxHeight = iconSize)
+
+ val icons =
+ measurables
+ .fastFilter { it.layoutId != Contents.Track }
+ .associateBy(
+ keySelector = { it.layoutId as Contents },
+ valueTransform = { it.measure(iconConstraints) },
+ )
+
+ return layout(track.width, track.height) {
+ with(Contents.Track) {
+ performPlacing(
+ placeable = track,
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+ }
+
+ for (iconLayoutId in icons.keys) {
+ with(iconLayoutId) {
+ performPlacing(
+ placeable = icons.getValue(iconLayoutId),
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+
+ isVisible.getValue(iconLayoutId).value =
+ isVisible(
+ placeable = icons.getValue(iconLayoutId),
+ width = track.width,
+ height = track.height,
+ sliderState = sliderState,
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+private sealed interface Contents {
+
+ data object Track : Contents {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = placeable.place(x = 0, y = 0)
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = true
+
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color =
+ error("Unsupported")
+ }
+
+ interface Active : Contents {
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
+ return if (isEnabled) {
+ sliderColors.activeTickColor
+ } else {
+ sliderColors.disabledActiveTickColor
+ }
+ }
+
+ data object TrackStartIcon : Active {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) =
+ placeable.place(
+ x = 0,
+ y = (height * (1 - sliderState.coercedValueAsFraction)).toInt(),
+ )
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+
+ data object TrackEndIcon : Active {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) = placeable.place(x = 0, y = (height - placeable.height))
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean = (height * (sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+ }
+
+ interface Inactive : Contents {
+
+ override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
+ return if (isEnabled) {
+ sliderColors.inactiveTickColor
+ } else {
+ sliderColors.disabledInactiveTickColor
+ }
+ }
+
+ data object TrackStartIcon : Inactive {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) {
+ placeable.place(x = 0, y = 0)
+ }
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+
+ data object TrackEndIcon : Inactive {
+ override fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ) {
+ placeable.place(
+ x = 0,
+ y =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() -
+ placeable.height,
+ )
+ }
+
+ override fun isVisible(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ ): Boolean =
+ (height * (1 - sliderState.coercedValueAsFraction)).toInt() > placeable.height
+ }
+ }
+
+ fun Placeable.PlacementScope.performPlacing(
+ placeable: Placeable,
+ width: Int,
+ height: Int,
+ sliderState: SliderState,
+ )
+
+ fun isVisible(placeable: Placeable, width: Int, height: Int, sliderState: SliderState): Boolean
+
+ fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color
+}
+
+/** Provides visibility state for each of the Slider's icons. */
+interface SliderIconsState {
+ val isActiveTrackStartIconVisible: Boolean
+ val isActiveTrackEndIconVisible: Boolean
+ val isInactiveTrackStartIconVisible: Boolean
+ val isInactiveTrackEndIconVisible: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
index 8df9e788905c..b01046b377b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt
@@ -20,17 +20,20 @@ import android.graphics.drawable.Drawable
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
data class VolumeDialogSliderStateModel(
- val minValue: Float,
- val maxValue: Float,
val value: Float,
+ val isDisabled: Boolean,
+ val valueRange: ClosedFloatingPointRange<Float>,
val icon: Drawable,
)
-fun VolumeDialogStreamModel.toStateModel(icon: Drawable): VolumeDialogSliderStateModel {
+fun VolumeDialogStreamModel.toStateModel(
+ isDisabled: Boolean,
+ icon: Drawable,
+): VolumeDialogSliderStateModel {
return VolumeDialogSliderStateModel(
- minValue = levelMin.toFloat(),
value = level.toFloat(),
- maxValue = levelMax.toFloat(),
+ isDisabled = isDisabled,
+ valueRange = levelMin.toFloat()..levelMax.toFloat(),
icon = icon,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
index a752f1f78e74..df9b32c9326f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -26,17 +26,18 @@ import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import javax.inject.Inject
+import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
@@ -77,11 +78,12 @@ constructor(
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
.filterNotNull()
- val isDisabledByZenMode: Flow<Boolean> = interactor.isDisabledByZenMode
val state: Flow<VolumeDialogSliderStateModel> =
- model
- .flatMapLatest { streamModel ->
- with(streamModel) {
+ combine(
+ interactor.isDisabledByZenMode,
+ model,
+ model.flatMapLatest { streamModel ->
+ with(streamModel) {
val isMuted = muteSupported && muted
when (sliderType) {
is VolumeDialogSliderType.Stream ->
@@ -101,7 +103,9 @@ constructor(
}
}
}
- .map { icon -> streamModel.toStateModel(icon) }
+ },
+ ) { isDisabledByZenMode, model, icon ->
+ model.toStateModel(icon = icon, isDisabled = isDisabledByZenMode)
}
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
.filterNotNull()
@@ -116,11 +120,14 @@ constructor(
.launchIn(coroutineScope)
}
- fun setStreamVolume(volume: Int, fromUser: Boolean) {
+ fun setStreamVolume(volume: Float, fromUser: Boolean) {
if (fromUser) {
visibilityInteractor.resetDismissTimeout()
userVolumeUpdates.value =
- VolumeUpdate(newVolumeLevel = volume, timestampMillis = getTimestampMillis())
+ VolumeUpdate(
+ newVolumeLevel = volume.roundToInt(),
+ timestampMillis = getTimestampMillis(),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt
new file mode 100644
index 000000000000..92e9bf2d1ffc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/haptics/ui/VolumeHapticsConfigsProvider.kt
@@ -0,0 +1,41 @@
+/*
+ * 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)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
index 36fa82f82f0d..4ca044d60f3f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/dagger/VolumeDialogSliderComponentKosmos.kt
@@ -32,10 +32,8 @@ import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibility
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
import com.android.systemui.volume.dialog.sliders.ui.volumeDialogOverscrollViewBinder
-import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderHapticsViewBinder
import com.android.systemui.volume.dialog.sliders.ui.volumeDialogSliderViewBinder
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.mediaControllerInteractor
@@ -65,9 +63,6 @@ fun Kosmos.volumeDialogSliderComponent(type: VolumeDialogSliderType): VolumeDial
override fun sliderViewBinder(): VolumeDialogSliderViewBinder =
localKosmos.volumeDialogSliderViewBinder
- override fun sliderHapticsViewBinder(): VolumeDialogSliderHapticsViewBinder =
- localKosmos.volumeDialogSliderHapticsViewBinder
-
override fun overscrollViewBinder(): VolumeDialogOverscrollViewBinder =
localKosmos.volumeDialogOverscrollViewBinder
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
deleted file mode 100644
index d6845b1ff7e3..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderHapticsViewBinderKosmos.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2024 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.dialog.sliders.ui
-
-import com.android.systemui.haptics.msdl.msdlPlayer
-import com.android.systemui.haptics.vibratorHelper
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.time.systemClock
-import com.android.systemui.volume.dialog.sliders.ui.viewmodel.volumeDialogSliderInputEventsViewModel
-
-val Kosmos.volumeDialogSliderHapticsViewBinder by
- Kosmos.Fixture {
- VolumeDialogSliderHapticsViewBinder(
- volumeDialogSliderInputEventsViewModel,
- vibratorHelper,
- msdlPlayer,
- systemClock,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
index c6db717e004f..c7cbda1b0d49 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinderKosmos.kt
@@ -16,7 +16,9 @@
package com.android.systemui.volume.dialog.sliders.ui
+import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
+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
@@ -24,6 +26,8 @@ val Kosmos.volumeDialogSliderViewBinder by
Kosmos.Fixture {
VolumeDialogSliderViewBinder(
volumeDialogSliderViewModel,
+ volumeDialogOverscrollViewModel,
volumeDialogSliderInputEventsViewModel,
+ sliderHapticsViewModelFactory,
)
}