Update Volume Panel sliders to match the updated design

This change makes sure the VolumeDialogSliderTrack supports rtl and is
suitable for horizontal sliders

Flag: com.android.systemui.volume_redesign
Fixes: 399342224
Test: manual on foldable. Open Volume Panel and observe the sliders
Test: atest VolumePanelScreenshotTest
Test: atest VolumeDialogScreenshotTest
Change-Id: I43480d6c28cb9c63a2f3d508a53141f44251ad68
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
index df50eb8..da07fbd 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt
@@ -101,10 +101,17 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     colors: IconButtonColors = iconButtonColors(),
+    shape: Shape = IconButtonDefaults.standardShape,
     @DrawableRes iconResource: Int,
     contentDescription: String?,
 ) {
-    IconButton(modifier = modifier, onClick = onClick, enabled = enabled, colors = colors) {
+    IconButton(
+        modifier = modifier,
+        onClick = onClick,
+        enabled = enabled,
+        colors = colors,
+        shape = shape,
+    ) {
         Icon(
             painter = painterResource(id = iconResource),
             contentDescription = contentDescription,
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 d9e8f02..52b1e3a 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
@@ -30,9 +30,11 @@
 import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButtonDefaults
@@ -42,7 +44,6 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.semantics.Role
@@ -154,9 +155,7 @@
                                                 totalCount = viewModels.size,
                                             ),
                                     )
-                                    .thenIf(!Flags.volumeRedesign()) {
-                                        Modifier.padding(top = 16.dp)
-                                    },
+                                    .padding(top = if (Flags.volumeRedesign()) 4.dp else 16.dp),
                             state = sliderState,
                             onValueChange = { newValue: Float ->
                                 sliderViewModel.onValueChanged(sliderState, newValue)
@@ -223,7 +222,7 @@
 }
 
 @Composable
-private fun ExpandButton(
+private fun RowScope.ExpandButton(
     isExpanded: Boolean,
     isExpandable: Boolean,
     onExpandedChanged: (Boolean) -> Unit,
@@ -243,16 +242,17 @@
     ) {
         PlatformIconButton(
             modifier =
-                Modifier.size(width = 48.dp, height = 40.dp).semantics {
+                Modifier.size(40.dp).semantics {
                     role = Role.Switch
                     stateDescription = expandButtonStateDescription
                 },
             onClick = { onExpandedChanged(!isExpanded) },
             colors =
                 IconButtonDefaults.iconButtonColors(
-                    containerColor = Color.Transparent,
-                    contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
+                    containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+                    contentColor = MaterialTheme.colorScheme.onSurface,
                 ),
+            shape = RoundedCornerShape(12.dp),
             iconResource =
                 if (isExpanded) {
                     R.drawable.ic_arrow_down_24dp
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 da54cb8..f9492da 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
@@ -30,14 +30,16 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 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 as MaterialIcon
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SliderDefaults
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -58,31 +60,33 @@
 import androidx.compose.ui.semantics.customActions
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.progressBarRangeInfo
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.setProgress
 import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.DpSize
 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.shared.model.Icon as IconModel
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.compose.modifiers.sysuiResTag
 import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
 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.ui.compose.SliderTrack
 import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
 import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
-import com.android.systemui.volume.ui.slider.AccessibilityParams
-import com.android.systemui.volume.ui.slider.Haptics
-import com.android.systemui.volume.ui.slider.Slider
+import com.android.systemui.volume.ui.compose.slider.AccessibilityParams
+import com.android.systemui.volume.ui.compose.slider.Haptics
+import com.android.systemui.volume.ui.compose.slider.Slider
+import com.android.systemui.volume.ui.compose.slider.SliderIcon
 import kotlin.math.round
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun VolumeSlider(
     state: SliderState,
@@ -92,7 +96,7 @@
     modifier: Modifier = Modifier,
     hapticsViewModelFactory: SliderHapticsViewModel.Factory?,
     onValueChangeFinished: (() -> Unit)? = null,
-    button: (@Composable () -> Unit)? = null,
+    button: (@Composable RowScope.() -> Unit)? = null,
 ) {
     if (!Flags.volumeRedesign()) {
         LegacyVolumeSlider(
@@ -107,54 +111,86 @@
         return
     }
 
-    Column(
-        modifier = modifier.animateContentSize().semantics(true) {},
-        verticalArrangement = Arrangement.Top,
-    ) {
+    Column(modifier = modifier.animateContentSize()) {
+        Text(
+            text = state.label,
+            style = MaterialTheme.typography.titleMedium,
+            color = MaterialTheme.colorScheme.onSurface,
+            modifier = Modifier.fillMaxWidth().clearAndSetSemantics {},
+        )
         Row(
-            horizontalArrangement = Arrangement.spacedBy(12.dp),
-            modifier = Modifier.fillMaxWidth().height(40.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
             verticalAlignment = Alignment.CenterVertically,
         ) {
-            state.icon?.let {
-                Icon(
-                    icon = it,
-                    tint = MaterialTheme.colorScheme.onSurface,
-                    modifier = Modifier.size(24.dp),
+            val materialSliderColors =
+                SliderDefaults.colors(
+                    activeTickColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+                    inactiveTrackColor = MaterialTheme.colorScheme.surfaceContainerHigh,
                 )
-            }
-            Text(
-                text = state.label,
-                style = MaterialTheme.typography.titleMedium,
-                color = MaterialTheme.colorScheme.onSurface,
-                modifier = Modifier.weight(1f).clearAndSetSemantics {},
-            )
-            button?.invoke()
-        }
-
-        Slider(
-            value = state.value,
-            valueRange = state.valueRange,
-            onValueChanged = onValueChange,
-            onValueChangeFinished = { onValueChangeFinished?.invoke() },
-            isEnabled = state.isEnabled,
-            stepDistance = state.step,
-            accessibilityParams =
-                AccessibilityParams(
-                    contentDescription = state.a11yContentDescription,
-                    stateDescription = state.a11yStateDescription,
-                ),
-            haptics =
-                hapticsViewModelFactory?.let {
-                    Haptics.Enabled(
-                        hapticsViewModelFactory = it,
-                        hapticFilter = state.hapticFilter,
-                        orientation = Orientation.Horizontal,
+            Slider(
+                value = state.value,
+                valueRange = state.valueRange,
+                onValueChanged = onValueChange,
+                onValueChangeFinished = { onValueChangeFinished?.invoke() },
+                colors = materialSliderColors,
+                isEnabled = state.isEnabled,
+                stepDistance = state.step,
+                accessibilityParams =
+                    AccessibilityParams(
+                        contentDescription = state.a11yContentDescription,
+                        stateDescription = state.a11yStateDescription,
+                    ),
+                track = { sliderState ->
+                    SliderTrack(
+                        sliderState = sliderState,
+                        colors = materialSliderColors,
+                        isEnabled = state.isEnabled,
+                        activeTrackStartIcon =
+                            state.icon?.let { icon ->
+                                { iconsState ->
+                                    SliderIcon(
+                                        icon = {
+                                            Icon(icon = icon, modifier = Modifier.size(24.dp))
+                                        },
+                                        isVisible = iconsState.isActiveTrackStartIconVisible,
+                                    )
+                                }
+                            },
+                        inactiveTrackStartIcon =
+                            state.icon?.let { icon ->
+                                { iconsState ->
+                                    SliderIcon(
+                                        icon = {
+                                            Icon(icon = icon, modifier = Modifier.size(24.dp))
+                                        },
+                                        isVisible = !iconsState.isActiveTrackStartIconVisible,
+                                    )
+                                }
+                            },
                     )
-                } ?: Haptics.Disabled,
-            modifier =
-                Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
-        )
+                },
+                thumb = { sliderState, interactionSource ->
+                    SliderDefaults.Thumb(
+                        sliderState = sliderState,
+                        interactionSource = interactionSource,
+                        enabled = state.isEnabled,
+                        colors = materialSliderColors,
+                        thumbSize = DpSize(4.dp, 52.dp),
+                    )
+                },
+                haptics =
+                    hapticsViewModelFactory?.let {
+                        Haptics.Enabled(
+                            hapticsViewModelFactory = it,
+                            hapticFilter = state.hapticFilter,
+                            orientation = Orientation.Horizontal,
+                        )
+                    } ?: Haptics.Disabled,
+                modifier = Modifier.weight(1f).sysuiResTag(state.label),
+            )
+            button?.invoke(this)
+        }
         state.disabledMessage?.let { disabledMessage ->
             AnimatedVisibility(visible = !state.isEnabled) {
                 Row(
@@ -253,7 +289,11 @@
         enabled = state.isEnabled,
         icon = {
             state.icon?.let {
-                SliderIcon(icon = it, onIconTapped = onIconTapped, isTappable = state.isMutable)
+                LegacySliderIcon(
+                    icon = it,
+                    onIconTapped = onIconTapped,
+                    isTappable = state.isMutable,
+                )
             }
         },
         colors = sliderColors,
@@ -289,8 +329,8 @@
 }
 
 @Composable
-private fun SliderIcon(
-    icon: Icon,
+private fun LegacySliderIcon(
+    icon: IconModel,
     onIconTapped: () -> Unit,
     isTappable: Boolean,
     modifier: Modifier = Modifier,
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 32f784f..db4b8ef 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
@@ -17,42 +17,37 @@
 package com.android.systemui.volume.dialog.sliders.ui
 
 import android.view.View
-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.DragInteraction
 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.MaterialTheme
 import androidx.compose.material3.SliderDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import com.android.compose.theme.PlatformTheme
-import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.common.ui.compose.Icon
 import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
 import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
 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.compose.SliderTrack
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
 import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.android.systemui.volume.ui.slider.AccessibilityParams
-import com.android.systemui.volume.ui.slider.Haptics
-import com.android.systemui.volume.ui.slider.Slider
+import com.android.systemui.volume.ui.compose.slider.AccessibilityParams
+import com.android.systemui.volume.ui.compose.slider.Haptics
+import com.android.systemui.volume.ui.compose.slider.Slider
+import com.android.systemui.volume.ui.compose.slider.SliderIcon
 import javax.inject.Inject
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
@@ -85,7 +80,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun VolumeDialogSlider(
     viewModel: VolumeDialogSliderViewModel,
@@ -95,10 +90,7 @@
 ) {
     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 collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
@@ -142,18 +134,38 @@
             } ?: Haptics.Disabled,
         stepDistance = 1f,
         track = { sliderState ->
-            VolumeDialogSliderTrack(
+            SliderTrack(
                 sliderState,
                 colors = colors,
                 isEnabled = !sliderStateModel.isDisabled,
-                activeTrackEndIcon = { iconsState ->
-                    VolumeIcon(sliderStateModel.icon, iconsState.isActiveTrackEndIconVisible)
+                isVertical = true,
+                activeTrackStartIcon = { iconsState ->
+                    SliderIcon(
+                        icon = {
+                            Icon(icon = sliderStateModel.icon, modifier = Modifier.size(20.dp))
+                        },
+                        isVisible = iconsState.isActiveTrackStartIconVisible,
+                    )
                 },
-                inactiveTrackEndIcon = { iconsState ->
-                    VolumeIcon(sliderStateModel.icon, !iconsState.isActiveTrackEndIconVisible)
+                inactiveTrackStartIcon = { iconsState ->
+                    SliderIcon(
+                        icon = {
+                            Icon(icon = sliderStateModel.icon, modifier = Modifier.size(20.dp))
+                        },
+                        isVisible = !iconsState.isActiveTrackStartIconVisible,
+                    )
                 },
             )
         },
+        thumb = { sliderState, interactions ->
+            SliderDefaults.Thumb(
+                sliderState = sliderState,
+                interactionSource = interactions,
+                enabled = !sliderStateModel.isDisabled,
+                colors = colors,
+                thumbSize = DpSize(52.dp, 4.dp),
+            )
+        },
         accessibilityParams = AccessibilityParams(contentDescription = sliderStateModel.label),
         modifier =
             modifier.pointerInput(Unit) {
@@ -168,19 +180,3 @@
             },
     )
 }
-
-@Composable
-private fun BoxScope.VolumeIcon(
-    icon: Icon.Loaded,
-    isVisible: Boolean,
-    modifier: Modifier = Modifier,
-) {
-    AnimatedVisibility(
-        visible = isVisible,
-        enter = fadeIn(animationSpec = tween(delayMillis = 33, durationMillis = 100)),
-        exit = fadeOut(animationSpec = tween(durationMillis = 50)),
-        modifier = modifier.align(Alignment.Center).size(40.dp).padding(10.dp),
-    ) {
-        Icon(icon)
-    }
-}
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
index 1dd9dda..fb8de45 100644
--- 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
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -32,38 +33,47 @@
 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.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
 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(
+fun SliderTrack(
     sliderState: SliderState,
-    colors: SliderColors,
     isEnabled: Boolean,
     modifier: Modifier = Modifier,
+    colors: SliderColors = SliderDefaults.colors(),
     thumbTrackGapSize: Dp = 6.dp,
     trackCornerSize: Dp = 12.dp,
     trackInsideCornerSize: Dp = 2.dp,
     trackSize: Dp = 40.dp,
+    isVertical: Boolean = false,
     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) }
+    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+    val measurePolicy =
+        remember(sliderState) {
+            TrackMeasurePolicy(
+                sliderState = sliderState,
+                shouldMirrorIcons = !isVertical && isRtl || isVertical,
+                isVertical = isVertical,
+                gapSize = thumbTrackGapSize,
+            )
+        }
     Layout(
         measurePolicy = measurePolicy,
         content = {
@@ -76,33 +86,41 @@
                 drawStopIndicator = null,
                 thumbTrackGapSize = thumbTrackGapSize,
                 drawTick = { _, _ -> },
-                modifier = Modifier.width(trackSize).layoutId(Contents.Track),
+                modifier =
+                    Modifier.then(
+                            if (isVertical) {
+                                Modifier.width(trackSize)
+                            } else {
+                                Modifier.height(trackSize)
+                            }
+                        )
+                        .layoutId(Contents.Track),
             )
 
             TrackIcon(
                 icon = activeTrackStartIcon,
-                contentsId = Contents.Active.TrackStartIcon,
+                contents = Contents.Active.TrackStartIcon,
                 isEnabled = isEnabled,
                 colors = colors,
                 state = measurePolicy,
             )
             TrackIcon(
                 icon = activeTrackEndIcon,
-                contentsId = Contents.Active.TrackEndIcon,
+                contents = Contents.Active.TrackEndIcon,
                 isEnabled = isEnabled,
                 colors = colors,
                 state = measurePolicy,
             )
             TrackIcon(
                 icon = inactiveTrackStartIcon,
-                contentsId = Contents.Inactive.TrackStartIcon,
+                contents = Contents.Inactive.TrackStartIcon,
                 isEnabled = isEnabled,
                 colors = colors,
                 state = measurePolicy,
             )
             TrackIcon(
                 icon = inactiveTrackEndIcon,
-                contentsId = Contents.Inactive.TrackEndIcon,
+                contents = Contents.Inactive.TrackEndIcon,
                 isEnabled = isEnabled,
                 colors = colors,
                 state = measurePolicy,
@@ -116,24 +134,47 @@
 private fun TrackIcon(
     icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?,
     isEnabled: Boolean,
-    contentsId: Contents,
+    contents: 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)
+    /*
+    ignore icons mirroring for the rtl layouts here because icons positioning is handled by the
+    TrackMeasurePolicy. It ensures that active icons are always above the active track and the
+    same for inactive
+    */
+    val iconColor =
+        when (contents) {
+            is Contents.Inactive ->
+                if (isEnabled) {
+                    colors.inactiveTickColor
+                } else {
+                    colors.disabledInactiveTickColor
+                }
+            is Contents.Active ->
+                if (isEnabled) {
+                    colors.activeTickColor
+                } else {
+                    colors.disabledActiveTickColor
+                }
+            is Contents.Track -> {
+                error("$contents is unsupported by the TrackIcon")
+            }
         }
+    Box(modifier = modifier.layoutId(contents).fillMaxSize()) {
+        CompositionLocalProvider(LocalContentColor provides iconColor) { icon(state) }
     }
 }
 
 @OptIn(ExperimentalMaterial3Api::class)
-private class TrackMeasurePolicy(private val sliderState: SliderState) :
-    MeasurePolicy, SliderIconsState {
+private class TrackMeasurePolicy(
+    private val sliderState: SliderState,
+    private val shouldMirrorIcons: Boolean,
+    private val gapSize: Dp,
+    private val isVertical: Boolean,
+) : MeasurePolicy, SliderIconsState {
 
     private val isVisible: Map<Contents, MutableState<Boolean>> =
         mutableMapOf(
@@ -144,16 +185,16 @@
         )
 
     override val isActiveTrackStartIconVisible: Boolean
-        get() = isVisible.getValue(Contents.Active.TrackStartIcon).value
+        get() = isVisible.getValue(Contents.Active.TrackStartIcon.resolve()).value
 
     override val isActiveTrackEndIconVisible: Boolean
-        get() = isVisible.getValue(Contents.Active.TrackEndIcon).value
+        get() = isVisible.getValue(Contents.Active.TrackEndIcon.resolve()).value
 
     override val isInactiveTrackStartIconVisible: Boolean
-        get() = isVisible.getValue(Contents.Inactive.TrackStartIcon).value
+        get() = isVisible.getValue(Contents.Inactive.TrackStartIcon.resolve()).value
 
     override val isInactiveTrackEndIconVisible: Boolean
-        get() = isVisible.getValue(Contents.Inactive.TrackEndIcon).value
+        get() = isVisible.getValue(Contents.Inactive.TrackEndIcon.resolve()).value
 
     override fun MeasureScope.measure(
         measurables: List<Measurable>,
@@ -164,178 +205,196 @@
         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) },
-                )
+        val components = buildMap {
+            put(Contents.Track, track)
+            for (measurable in measurables) {
+                // don't measure track a second time
+                if (measurable.layoutId != Contents.Track) {
+                    put(
+                        (measurable.layoutId as Contents).resolve(),
+                        measurable.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,
+            val gapSizePx = gapSize.roundToPx()
+            val coercedValueAsFraction =
+                if (shouldMirrorIcons) {
+                    1 - sliderState.coercedValueAsFraction
+                } else {
+                    sliderState.coercedValueAsFraction
+                }
+            for (iconLayoutId in components.keys) {
+                val iconPlaceable = components.getValue(iconLayoutId)
+                if (isVertical) {
+                    iconPlaceable.place(
+                        0,
+                        iconLayoutId.calculatePosition(
+                            placeableDimension = iconPlaceable.height,
+                            containerDimension = track.height,
+                            gapSize = gapSizePx,
+                            coercedValueAsFraction = coercedValueAsFraction,
+                        ),
                     )
+                } else {
+                    iconPlaceable.place(
+                        iconLayoutId.calculatePosition(
+                            placeableDimension = iconPlaceable.width,
+                            containerDimension = track.width,
+                            gapSize = gapSizePx,
+                            coercedValueAsFraction = coercedValueAsFraction,
+                        ),
+                        0,
+                    )
+                }
 
-                    isVisible.getValue(iconLayoutId).value =
-                        isVisible(
-                            placeable = icons.getValue(iconLayoutId),
-                            width = track.width,
-                            height = track.height,
-                            sliderState = sliderState,
+                // isVisible is only relevant for the icons
+                if (iconLayoutId != Contents.Track) {
+                    val isVisibleState = isVisible.getValue(iconLayoutId)
+                    val newIsVisible =
+                        iconLayoutId.isVisible(
+                            placeableDimension =
+                                if (isVertical) iconPlaceable.height else iconPlaceable.width,
+                            containerDimension = if (isVertical) track.height else track.width,
+                            gapSize = gapSizePx,
+                            coercedValueAsFraction = coercedValueAsFraction,
                         )
+                    if (isVisibleState.value != newIsVisible) {
+                        isVisibleState.value = newIsVisible
+                    }
                 }
             }
         }
     }
+
+    private fun Contents.resolve(): Contents {
+        return if (shouldMirrorIcons) {
+            mirrored
+        } else {
+            this
+        }
+    }
 }
 
-@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 val mirrored: Contents
+            get() = error("unsupported for Track")
+
+        override fun calculatePosition(
+            placeableDimension: Int,
+            containerDimension: Int,
+            gapSize: Int,
+            coercedValueAsFraction: Float,
+        ): Int = 0
 
         override fun isVisible(
-            placeable: Placeable,
-            width: Int,
-            height: Int,
-            sliderState: SliderState,
-        ) = true
-
-        override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color =
-            error("Unsupported")
+            placeableDimension: Int,
+            containerDimension: Int,
+            gapSize: Int,
+            coercedValueAsFraction: Float,
+        ): Boolean = true
     }
 
     interface Active : Contents {
-        override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
-            return if (isEnabled) {
-                sliderColors.activeTickColor
-            } else {
-                sliderColors.disabledActiveTickColor
-            }
-        }
+
+        override fun isVisible(
+            placeableDimension: Int,
+            containerDimension: Int,
+            gapSize: Int,
+            coercedValueAsFraction: Float,
+        ): Boolean =
+            (containerDimension * coercedValueAsFraction - gapSize).toInt() > placeableDimension
 
         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
+            override val mirrored: Contents
+                get() = Inactive.TrackEndIcon
+
+            override fun calculatePosition(
+                placeableDimension: Int,
+                containerDimension: Int,
+                gapSize: Int,
+                coercedValueAsFraction: Float,
+            ): Int = 0
         }
 
         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
+            override val mirrored: Contents
+                get() = Inactive.TrackStartIcon
+
+            override fun calculatePosition(
+                placeableDimension: Int,
+                containerDimension: Int,
+                gapSize: Int,
+                coercedValueAsFraction: Float,
+            ): Int =
+                (containerDimension * coercedValueAsFraction - placeableDimension - gapSize).toInt()
         }
     }
 
     interface Inactive : Contents {
 
-        override fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color {
-            return if (isEnabled) {
-                sliderColors.inactiveTickColor
-            } else {
-                sliderColors.disabledInactiveTickColor
-            }
-        }
+        override fun isVisible(
+            placeableDimension: Int,
+            containerDimension: Int,
+            gapSize: Int,
+            coercedValueAsFraction: Float,
+        ): Boolean =
+            containerDimension - (containerDimension * coercedValueAsFraction + gapSize) >
+                placeableDimension
 
         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
+            override val mirrored: Contents
+                get() = Active.TrackEndIcon
+
+            override fun calculatePosition(
+                placeableDimension: Int,
+                containerDimension: Int,
+                gapSize: Int,
+                coercedValueAsFraction: Float,
+            ): Int = (containerDimension * coercedValueAsFraction + gapSize).toInt()
         }
 
         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
+            override val mirrored: Contents
+                get() = Active.TrackStartIcon
+
+            override fun calculatePosition(
+                placeableDimension: Int,
+                containerDimension: Int,
+                gapSize: Int,
+                coercedValueAsFraction: Float,
+            ): Int = containerDimension - placeableDimension
         }
     }
 
-    fun Placeable.PlacementScope.performPlacing(
-        placeable: Placeable,
-        width: Int,
-        height: Int,
-        sliderState: SliderState,
-    )
+    fun calculatePosition(
+        placeableDimension: Int,
+        containerDimension: Int,
+        gapSize: Int,
+        coercedValueAsFraction: Float,
+    ): Int
 
-    fun isVisible(placeable: Placeable, width: Int, height: Int, sliderState: SliderState): Boolean
+    fun isVisible(
+        placeableDimension: Int,
+        containerDimension: Int,
+        gapSize: Int,
+        coercedValueAsFraction: Float,
+    ): Boolean
 
-    fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color
+    /**
+     * [Contents] that is visually on the opposite side of the current one on the slider. This is
+     * handy when dealing with the rtl layouts
+     */
+    val mirrored: Contents
 }
 
 /** Provides visibility state for each of the Slider's icons. */
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
rename to packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt
index 502b311..54d2f79 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt
@@ -16,7 +16,7 @@
 
 @file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 
-package com.android.systemui.volume.ui.slider
+package com.android.systemui.volume.ui.compose.slider
 
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.Spring
@@ -61,8 +61,6 @@
 
 private val defaultSpring =
     SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)
-private val defaultTrack: @Composable (SliderState) -> Unit =
-    @Composable { SliderDefaults.Track(it) }
 
 @Composable
 fun Slider(
@@ -79,7 +77,14 @@
     haptics: Haptics = Haptics.Disabled,
     isVertical: Boolean = false,
     isReverseDirection: Boolean = false,
-    track: (@Composable (SliderState) -> Unit)? = null,
+    track: (@Composable (SliderState) -> Unit) = { SliderDefaults.Track(it) },
+    thumb: (@Composable (SliderState, MutableInteractionSource) -> Unit) = { _, _ ->
+        SliderDefaults.Thumb(
+            interactionSource = interactionSource,
+            colors = colors,
+            enabled = isEnabled,
+        )
+    },
 ) {
     require(stepDistance >= 0) { "stepDistance must not be negative" }
     val coroutineScope = rememberCoroutineScope()
@@ -139,7 +144,8 @@
             reverseDirection = isReverseDirection,
             interactionSource = interactionSource,
             colors = colors,
-            track = track ?: defaultTrack,
+            track = track,
+            thumb = { thumb(it, interactionSource) },
             modifier = modifier.clearAndSetSemantics(semantics),
         )
     } else {
@@ -148,7 +154,8 @@
             enabled = isEnabled,
             interactionSource = interactionSource,
             colors = colors,
-            track = track ?: defaultTrack,
+            track = track,
+            thumb = { thumb(it, interactionSource) },
             modifier = modifier.clearAndSetSemantics(semantics),
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/SliderIcon.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/SliderIcon.kt
new file mode 100644
index 0000000..fd8f477
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/SliderIcon.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.ui.compose.slider
+
+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.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun SliderIcon(
+    icon: @Composable BoxScope.() -> Unit,
+    isVisible: Boolean,
+    modifier: Modifier = Modifier,
+) {
+    Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) {
+        AnimatedVisibility(
+            visible = isVisible,
+            enter = fadeIn(animationSpec = tween(delayMillis = 33, durationMillis = 100)),
+            exit = fadeOut(animationSpec = tween(durationMillis = 50)),
+        ) {
+            icon()
+        }
+    }
+}