diff options
author | 2025-03-14 12:40:15 -0700 | |
---|---|---|
committer | 2025-03-14 12:40:15 -0700 | |
commit | 87ae0365b9f784fe29f0c3079c20d4d050a47e78 (patch) | |
tree | 9103e9929e8c0acc2ec65ebed93c0b9012be3c2b | |
parent | d8acb5d5102ab01f7b1f82921b09e4242e3b21ec (diff) | |
parent | 4fe2d5aa75350770074f465774d152639d3581ba (diff) |
Merge "Update Volume Panel sliders to match the updated design" into main
-rw-r--r-- | packages/SystemUI/compose/core/src/com/android/compose/PlatformButtons.kt | 9 | ||||
-rw-r--r-- | packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt | 16 | ||||
-rw-r--r-- | packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt | 146 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt | 70 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt | 371 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/Slider.kt (renamed from packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt) | 19 | ||||
-rw-r--r-- | packages/SystemUI/src/com/android/systemui/volume/ui/compose/slider/SliderIcon.kt | 45 |
7 files changed, 415 insertions, 261 deletions
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 df50eb8fa3e8..da07fbd9b6a6 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 @@ fun PlatformIconButton( 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 d9e8f02f005b..52b1e3aeb00c 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.scaleOut 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.State 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 @@ fun ColumnVolumeSliders( 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 @@ private fun ExpandButtonLegacy( } @Composable -private fun ExpandButton( +private fun RowScope.ExpandButton( isExpanded: Boolean, isExpandable: Boolean, onExpandedChanged: (Boolean) -> Unit, @@ -243,16 +242,17 @@ private fun ExpandButton( ) { 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 da54cb8e4679..f9492dad85df 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.Arrangement 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.contentDescription 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 @@ fun VolumeSlider( 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 @@ fun VolumeSlider( 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 {}, + 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, + ) + } + }, + ) + }, + 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() + button?.invoke(this) } - - 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, - ) - } ?: Haptics.Disabled, - modifier = - Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label), - ) state.disabledMessage?.let { disabledMessage -> AnimatedVisibility(visible = !state.isEnabled) { Row( @@ -253,7 +289,11 @@ private fun LegacyVolumeSlider( 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 @@ private fun valueState(state: SliderState): State<Float> { } @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 32f784f17bb7..db4b8ef5aef7 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 @@ constructor( } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun VolumeDialogSlider( viewModel: VolumeDialogSliderViewModel, @@ -95,10 +90,7 @@ private fun VolumeDialogSlider( ) { 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 @@ private fun VolumeDialogSlider( } ?: 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 @@ private fun VolumeDialogSlider( }, ) } - -@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 1dd9ddac79be..fb8de45bfad1 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 @@ 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.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.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.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 @@ fun VolumeDialogSliderTrack( 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 @@ fun VolumeDialogSliderTrack( 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 @@ private class TrackMeasurePolicy(private val sliderState: SliderState) : ) 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 @@ private class TrackMeasurePolicy(private val sliderState: SliderState) : 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, - ) + 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), + ) + } } + } - for (iconLayoutId in icons.keys) { - with(iconLayoutId) { - performPlacing( - placeable = icons.getValue(iconLayoutId), - width = track.width, - height = track.height, - sliderState = sliderState, + return layout(track.width, track.height) { + 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 isVisible(placeable: Placeable, width: Int, height: Int, sliderState: SliderState): Boolean - - fun getColor(sliderColors: SliderColors, isEnabled: Boolean): Color + fun calculatePosition( + placeableDimension: Int, + containerDimension: Int, + gapSize: Int, + coercedValueAsFraction: Float, + ): Int + + fun isVisible( + placeableDimension: Int, + containerDimension: Int, + gapSize: Int, + coercedValueAsFraction: Float, + ): Boolean + + /** + * [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 index 502b311f7b40..54d2f79509c3 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 @@ import kotlinx.coroutines.launch 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 @@ fun Slider( 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 @@ fun Slider( reverseDirection = isReverseDirection, interactionSource = interactionSource, colors = colors, - track = track ?: defaultTrack, + track = track, + thumb = { thumb(it, interactionSource) }, modifier = modifier.clearAndSetSemantics(semantics), ) } else { @@ -148,7 +154,8 @@ fun Slider( 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 000000000000..fd8f47794fc0 --- /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() + } + } +} |