Loading packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +273 −108 Original line number Diff line number Diff line Loading @@ -21,22 +21,25 @@ package com.android.compose import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -44,17 +47,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.clipPath 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.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.android.compose.modifiers.padding import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import com.android.compose.theme.LocalAndroidColorScheme /** Loading @@ -62,15 +76,16 @@ import com.android.compose.theme.LocalAndroidColorScheme * * @param onValueChangeFinished is called when the slider settles on a [value]. This callback * shouldn't be used to react to value changes. Use [onValueChange] instead * @param interactionSource - the [MutableInteractionSource] representing the stream of Interactions * @param interactionSource the [MutableInteractionSource] representing the stream of Interactions * for this slider. You can create and pass in your own remembered instance to observe * Interactions and customize the appearance / behavior of this slider in different states. * @param colors - slider color scheme. * @param draggingCornersRadius - radius of the slider indicator when the user drags it * @param icon - icon at the start of the slider. Icon is limited to a square space at the start of * the slider * @param label - control shown next to the icon. * @param colors determine slider color scheme. * @param draggingCornersRadius is the radius of the slider indicator when the user drags it * @param icon at the start of the slider. Icon is limited to a square space at the start of the * slider * @param label is shown next to the icon. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun PlatformSlider( value: Float, Loading @@ -86,7 +101,7 @@ fun PlatformSlider( label: (@Composable (isDragging: Boolean) -> Unit)? = null, ) { val sliderHeight: Dp = 64.dp val iconWidth: Dp = sliderHeight val thumbSize: Dp = sliderHeight var isDragging by remember { mutableStateOf(false) } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> Loading @@ -101,16 +116,6 @@ fun PlatformSlider( } } } val paddingStart by animateDpAsState( targetValue = if ((!isDragging && value == valueRange.start) || icon == null) { 16.dp } else { 0.dp }, label = "LabelIconSpacingAnimation" ) Box(modifier = modifier.height(sliderHeight)) { Slider( Loading @@ -126,130 +131,275 @@ fun PlatformSlider( sliderState = it, enabled = enabled, colors = colors, iconWidth = iconWidth, draggingCornersRadius = draggingCornersRadius, sliderHeight = sliderHeight, thumbSize = thumbSize, isDragging = isDragging, modifier = Modifier, label = label, icon = icon, modifier = Modifier.fillMaxSize(), ) }, thumb = { Spacer(Modifier.width(iconWidth).height(sliderHeight)) }, thumb = { Spacer(Modifier.size(thumbSize)) }, ) Spacer( Modifier.padding(8.dp) .size(4.dp) .align(Alignment.CenterEnd) .background(color = colors.indicatorColor, shape = CircleShape) ) } } private enum class TrackComponent(val zIndex: Float) { Background(0f), Icon(1f), Label(1f), } if (icon != null || label != null) { Row(modifier = Modifier.fillMaxSize()) { icon?.let { iconComposable -> @Composable private fun Track( sliderState: SliderState, enabled: Boolean, colors: PlatformSliderColors, draggingCornersRadius: Dp, sliderHeight: Dp, thumbSize: Dp, isDragging: Boolean, icon: (@Composable (isDragging: Boolean) -> Unit)?, label: (@Composable (isDragging: Boolean) -> Unit)?, modifier: Modifier = Modifier, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl var drawingState: DrawingState by remember { mutableStateOf(DrawingState()) } Layout( modifier = modifier, content = { TrackBackground( modifier = Modifier.layoutId(TrackComponent.Background), drawingState = drawingState, enabled = enabled, colors = colors, draggingCornersRadiusActive = draggingCornersRadius, draggingCornersRadiusIdle = sliderHeight / 2, isDragging = isDragging, ) if (icon != null) { Box( modifier = Modifier.fillMaxHeight().aspectRatio(1f), modifier = Modifier.layoutId(TrackComponent.Icon).clip(CircleShape), contentAlignment = Alignment.Center, ) { iconComposable(isDragging) CompositionLocalProvider( LocalContentColor provides if (enabled) colors.iconColor else colors.disabledIconColor ) { icon(isDragging) } } label?.let { labelComposable -> } if (label != null) { val offsetX by animateFloatAsState( targetValue = if (enabled) { if (drawingState.isLabelOnTopOfIndicator) { drawingState.iconWidth.coerceAtLeast( LocalDensity.current.run { 16.dp.toPx() } ) } else { val indicatorWidth = drawingState.indicatorRight - drawingState.indicatorLeft indicatorWidth + LocalDensity.current.run { 16.dp.toPx() } } } else { drawingState.iconWidth }, label = "LabelIconSpacingAnimation" ) Box( modifier = Modifier.fillMaxHeight() .weight(1f) .padding( start = { paddingStart.roundToPx() }, end = { sliderHeight.roundToPx() / 2 }, ), Modifier.layoutId(TrackComponent.Label).offset { IntOffset(offsetX.toInt(), 0) }, contentAlignment = Alignment.CenterStart, ) { labelComposable(isDragging) } } CompositionLocalProvider( LocalContentColor provides colors.getLabelColor( isEnabled = enabled, isLabelOnTopOfTheIndicator = drawingState.isLabelOnTopOfIndicator, ) ) { label(isDragging) } } } }, measurePolicy = TrackMeasurePolicy( sliderState = sliderState, thumbSize = LocalDensity.current.run { thumbSize.roundToPx() }, isRtl = isRtl, onDrawingStateMeasured = { drawingState = it } ) ) } @Composable private fun Track( sliderState: SliderState, private fun TrackBackground( drawingState: DrawingState, enabled: Boolean, colors: PlatformSliderColors, iconWidth: Dp, draggingCornersRadius: Dp, sliderHeight: Dp, draggingCornersRadiusActive: Dp, draggingCornersRadiusIdle: Dp, isDragging: Boolean, modifier: Modifier = Modifier, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val iconWidthPx: Float val halfIconWidthPx: Float val targetIndicatorRadiusPx: Float val halfSliderHeightPx: Float with(LocalDensity.current) { halfSliderHeightPx = sliderHeight.toPx() / 2 iconWidthPx = iconWidth.toPx() halfIconWidthPx = iconWidthPx / 2 targetIndicatorRadiusPx = if (isDragging) draggingCornersRadius.toPx() else halfSliderHeightPx } val indicatorRadiusPx: Float by animateFloatAsState( targetValue = targetIndicatorRadiusPx, val indicatorRadiusDp: Dp by animateDpAsState( targetValue = if (isDragging) draggingCornersRadiusActive else draggingCornersRadiusIdle, label = "PlatformSliderCornersAnimation", ) val trackColor = colors.getTrackColor(enabled) val indicatorColor = colors.getIndicatorColor(enabled) val trackCornerRadius = CornerRadius(halfSliderHeightPx, halfSliderHeightPx) val indicatorCornerRadius = CornerRadius(indicatorRadiusPx, indicatorRadiusPx) Canvas(modifier.fillMaxSize()) { val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2) val trackPath = Path() trackPath.addRoundRect( RoundRect( left = -halfIconWidthPx, left = 0f, top = 0f, right = size.width + halfIconWidthPx, bottom = size.height, right = drawingState.totalWidth, bottom = drawingState.totalHeight, cornerRadius = trackCornerRadius, ) ) drawPath(path = trackPath, color = trackColor) val indicatorCornerRadius = CornerRadius(indicatorRadiusDp.toPx(), indicatorRadiusDp.toPx()) clipPath(trackPath) { val indicatorPath = Path() if (isRtl) { indicatorPath.addRoundRect( RoundRect( left = size.width - size.width * sliderState.coercedNormalizedValue - halfIconWidthPx, top = 0f, right = size.width + iconWidthPx, bottom = size.height, topLeftCornerRadius = indicatorCornerRadius, topRightCornerRadius = trackCornerRadius, bottomRightCornerRadius = trackCornerRadius, bottomLeftCornerRadius = indicatorCornerRadius, ) ) } else { indicatorPath.addRoundRect( RoundRect( left = -halfIconWidthPx, top = 0f, right = size.width * sliderState.coercedNormalizedValue + halfIconWidthPx, bottom = size.height, left = drawingState.indicatorLeft, top = drawingState.indicatorTop, right = drawingState.indicatorRight, bottom = drawingState.indicatorBottom, topLeftCornerRadius = trackCornerRadius, topRightCornerRadius = indicatorCornerRadius, bottomRightCornerRadius = indicatorCornerRadius, bottomLeftCornerRadius = trackCornerRadius, ) ) } drawPath(path = indicatorPath, color = indicatorColor) } } } /** Measures track components sizes and calls [onDrawingStateMeasured] when it's done. */ private class TrackMeasurePolicy( private val sliderState: SliderState, private val thumbSize: Int, private val isRtl: Boolean, private val onDrawingStateMeasured: (DrawingState) -> Unit, ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List<Measurable>, constraints: Constraints ): MeasureResult { // Slider adds a paddings to the Track to make spase for thumb val desiredWidth = constraints.maxWidth + thumbSize val desiredHeight = constraints.maxHeight val backgroundPlaceable: Placeable = measurables .fastFirst { it.layoutId == TrackComponent.Background } .measure(Constraints(desiredWidth, desiredWidth, desiredHeight, desiredHeight)) val iconPlaceable: Placeable? = measurables .fastFirstOrNull { it.layoutId == TrackComponent.Icon } ?.measure( Constraints( minWidth = desiredHeight, maxWidth = desiredHeight, minHeight = desiredHeight, maxHeight = desiredHeight, ) ) val iconSize = iconPlaceable?.width ?: 0 val labelMaxWidth = (desiredWidth - iconSize) / 2 val labelPlaceable: Placeable? = measurables .fastFirstOrNull { it.layoutId == TrackComponent.Label } ?.measure( Constraints( minWidth = 0, maxWidth = labelMaxWidth, minHeight = desiredHeight, maxHeight = desiredHeight, ) ) val drawingState = if (isRtl) { DrawingState( isRtl = true, totalWidth = desiredWidth.toFloat(), totalHeight = desiredHeight.toFloat(), indicatorLeft = (desiredWidth - iconSize) * (1 - sliderState.coercedNormalizedValue), indicatorTop = 0f, indicatorRight = desiredWidth.toFloat(), indicatorBottom = desiredHeight.toFloat(), iconWidth = iconSize.toFloat(), labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, ) } else { DrawingState( isRtl = false, totalWidth = desiredWidth.toFloat(), totalHeight = desiredHeight.toFloat(), indicatorLeft = 0f, indicatorTop = 0f, indicatorRight = iconSize + (desiredWidth - iconSize) * sliderState.coercedNormalizedValue, indicatorBottom = desiredHeight.toFloat(), iconWidth = iconSize.toFloat(), labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, ) } onDrawingStateMeasured(drawingState) return layout(desiredWidth, desiredHeight) { backgroundPlaceable.placeRelative(0, 0, TrackComponent.Background.zIndex) iconPlaceable?.placeRelative(0, 0, TrackComponent.Icon.zIndex) labelPlaceable?.placeRelative(0, 0, TrackComponent.Label.zIndex) } } } private data class DrawingState( val isRtl: Boolean = false, val totalWidth: Float = 0f, val totalHeight: Float = 0f, val indicatorLeft: Float = 0f, val indicatorTop: Float = 0f, val indicatorRight: Float = 0f, val indicatorBottom: Float = 0f, val iconWidth: Float = 0f, val labelWidth: Float = 0f, ) private val DrawingState.isLabelOnTopOfIndicator: Boolean get() = labelWidth < indicatorRight - indicatorLeft - iconWidth /** [SliderState.value] normalized using [SliderState.valueRange]. The result belongs to [0, 1] */ private val SliderState.coercedNormalizedValue: Float get() { Loading @@ -268,17 +418,19 @@ private val SliderState.coercedNormalizedValue: Float * @param trackColor fills the track of the slider. This is a "background" of the slider * @param indicatorColor fills the slider from the start to the value * @param iconColor is the default icon color * @param labelColor is the default icon color * @param labelColorOnIndicator is the label color for when it's shown on top of the indicator * @param labelColorOnTrack is the label color for when it's shown on top of the track * @param disabledTrackColor is the [trackColor] when the PlatformSlider#enabled == false * @param disabledIndicatorColor is the [indicatorColor] when the PlatformSlider#enabled == false * @param disabledIconColor is the [iconColor] when the PlatformSlider#enabled == false * @param disabledLabelColor is the [labelColor] when the PlatformSlider#enabled == false * @param disabledLabelColor is the label color when the PlatformSlider#enabled == false */ data class PlatformSliderColors( val trackColor: Color, val indicatorColor: Color, val iconColor: Color, val labelColor: Color, val labelColorOnIndicator: Color, val labelColorOnTrack: Color, val disabledTrackColor: Color, val disabledIndicatorColor: Color, val disabledIconColor: Color, Loading @@ -300,10 +452,11 @@ object PlatformSliderDefaults { @Composable private fun lightThemePlatformSliderColors() = PlatformSliderColors( trackColor = MaterialTheme.colorScheme.tertiaryContainer, indicatorColor = LocalAndroidColorScheme.current.tertiaryFixedDim, iconColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColor = MaterialTheme.colorScheme.onTertiaryContainer, trackColor = LocalAndroidColorScheme.current.tertiaryFixedDim, indicatorColor = MaterialTheme.colorScheme.tertiary, iconColor = MaterialTheme.colorScheme.onTertiary, labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, Loading @@ -314,10 +467,11 @@ private fun lightThemePlatformSliderColors() = @Composable private fun darkThemePlatformSliderColors() = PlatformSliderColors( trackColor = MaterialTheme.colorScheme.onTertiary, indicatorColor = LocalAndroidColorScheme.current.onTertiaryFixedVariant, trackColor = MaterialTheme.colorScheme.tertiary, indicatorColor = MaterialTheme.colorScheme.tertiary, iconColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, Loading @@ -329,3 +483,14 @@ private fun PlatformSliderColors.getTrackColor(isEnabled: Boolean): Color = private fun PlatformSliderColors.getIndicatorColor(isEnabled: Boolean): Color = if (isEnabled) indicatorColor else disabledIndicatorColor private fun PlatformSliderColors.getLabelColor( isEnabled: Boolean, isLabelOnTopOfTheIndicator: Boolean ): Color { return if (isEnabled) { if (isLabelOnTopOfTheIndicator) labelColorOnIndicator else labelColorOnTrack } else { disabledLabelColor } } Loading
packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +273 −108 Original line number Diff line number Diff line Loading @@ -21,22 +21,25 @@ package com.android.compose import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -44,17 +47,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.clipPath 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.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.android.compose.modifiers.padding import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import com.android.compose.theme.LocalAndroidColorScheme /** Loading @@ -62,15 +76,16 @@ import com.android.compose.theme.LocalAndroidColorScheme * * @param onValueChangeFinished is called when the slider settles on a [value]. This callback * shouldn't be used to react to value changes. Use [onValueChange] instead * @param interactionSource - the [MutableInteractionSource] representing the stream of Interactions * @param interactionSource the [MutableInteractionSource] representing the stream of Interactions * for this slider. You can create and pass in your own remembered instance to observe * Interactions and customize the appearance / behavior of this slider in different states. * @param colors - slider color scheme. * @param draggingCornersRadius - radius of the slider indicator when the user drags it * @param icon - icon at the start of the slider. Icon is limited to a square space at the start of * the slider * @param label - control shown next to the icon. * @param colors determine slider color scheme. * @param draggingCornersRadius is the radius of the slider indicator when the user drags it * @param icon at the start of the slider. Icon is limited to a square space at the start of the * slider * @param label is shown next to the icon. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun PlatformSlider( value: Float, Loading @@ -86,7 +101,7 @@ fun PlatformSlider( label: (@Composable (isDragging: Boolean) -> Unit)? = null, ) { val sliderHeight: Dp = 64.dp val iconWidth: Dp = sliderHeight val thumbSize: Dp = sliderHeight var isDragging by remember { mutableStateOf(false) } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> Loading @@ -101,16 +116,6 @@ fun PlatformSlider( } } } val paddingStart by animateDpAsState( targetValue = if ((!isDragging && value == valueRange.start) || icon == null) { 16.dp } else { 0.dp }, label = "LabelIconSpacingAnimation" ) Box(modifier = modifier.height(sliderHeight)) { Slider( Loading @@ -126,130 +131,275 @@ fun PlatformSlider( sliderState = it, enabled = enabled, colors = colors, iconWidth = iconWidth, draggingCornersRadius = draggingCornersRadius, sliderHeight = sliderHeight, thumbSize = thumbSize, isDragging = isDragging, modifier = Modifier, label = label, icon = icon, modifier = Modifier.fillMaxSize(), ) }, thumb = { Spacer(Modifier.width(iconWidth).height(sliderHeight)) }, thumb = { Spacer(Modifier.size(thumbSize)) }, ) Spacer( Modifier.padding(8.dp) .size(4.dp) .align(Alignment.CenterEnd) .background(color = colors.indicatorColor, shape = CircleShape) ) } } private enum class TrackComponent(val zIndex: Float) { Background(0f), Icon(1f), Label(1f), } if (icon != null || label != null) { Row(modifier = Modifier.fillMaxSize()) { icon?.let { iconComposable -> @Composable private fun Track( sliderState: SliderState, enabled: Boolean, colors: PlatformSliderColors, draggingCornersRadius: Dp, sliderHeight: Dp, thumbSize: Dp, isDragging: Boolean, icon: (@Composable (isDragging: Boolean) -> Unit)?, label: (@Composable (isDragging: Boolean) -> Unit)?, modifier: Modifier = Modifier, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl var drawingState: DrawingState by remember { mutableStateOf(DrawingState()) } Layout( modifier = modifier, content = { TrackBackground( modifier = Modifier.layoutId(TrackComponent.Background), drawingState = drawingState, enabled = enabled, colors = colors, draggingCornersRadiusActive = draggingCornersRadius, draggingCornersRadiusIdle = sliderHeight / 2, isDragging = isDragging, ) if (icon != null) { Box( modifier = Modifier.fillMaxHeight().aspectRatio(1f), modifier = Modifier.layoutId(TrackComponent.Icon).clip(CircleShape), contentAlignment = Alignment.Center, ) { iconComposable(isDragging) CompositionLocalProvider( LocalContentColor provides if (enabled) colors.iconColor else colors.disabledIconColor ) { icon(isDragging) } } label?.let { labelComposable -> } if (label != null) { val offsetX by animateFloatAsState( targetValue = if (enabled) { if (drawingState.isLabelOnTopOfIndicator) { drawingState.iconWidth.coerceAtLeast( LocalDensity.current.run { 16.dp.toPx() } ) } else { val indicatorWidth = drawingState.indicatorRight - drawingState.indicatorLeft indicatorWidth + LocalDensity.current.run { 16.dp.toPx() } } } else { drawingState.iconWidth }, label = "LabelIconSpacingAnimation" ) Box( modifier = Modifier.fillMaxHeight() .weight(1f) .padding( start = { paddingStart.roundToPx() }, end = { sliderHeight.roundToPx() / 2 }, ), Modifier.layoutId(TrackComponent.Label).offset { IntOffset(offsetX.toInt(), 0) }, contentAlignment = Alignment.CenterStart, ) { labelComposable(isDragging) } } CompositionLocalProvider( LocalContentColor provides colors.getLabelColor( isEnabled = enabled, isLabelOnTopOfTheIndicator = drawingState.isLabelOnTopOfIndicator, ) ) { label(isDragging) } } } }, measurePolicy = TrackMeasurePolicy( sliderState = sliderState, thumbSize = LocalDensity.current.run { thumbSize.roundToPx() }, isRtl = isRtl, onDrawingStateMeasured = { drawingState = it } ) ) } @Composable private fun Track( sliderState: SliderState, private fun TrackBackground( drawingState: DrawingState, enabled: Boolean, colors: PlatformSliderColors, iconWidth: Dp, draggingCornersRadius: Dp, sliderHeight: Dp, draggingCornersRadiusActive: Dp, draggingCornersRadiusIdle: Dp, isDragging: Boolean, modifier: Modifier = Modifier, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val iconWidthPx: Float val halfIconWidthPx: Float val targetIndicatorRadiusPx: Float val halfSliderHeightPx: Float with(LocalDensity.current) { halfSliderHeightPx = sliderHeight.toPx() / 2 iconWidthPx = iconWidth.toPx() halfIconWidthPx = iconWidthPx / 2 targetIndicatorRadiusPx = if (isDragging) draggingCornersRadius.toPx() else halfSliderHeightPx } val indicatorRadiusPx: Float by animateFloatAsState( targetValue = targetIndicatorRadiusPx, val indicatorRadiusDp: Dp by animateDpAsState( targetValue = if (isDragging) draggingCornersRadiusActive else draggingCornersRadiusIdle, label = "PlatformSliderCornersAnimation", ) val trackColor = colors.getTrackColor(enabled) val indicatorColor = colors.getIndicatorColor(enabled) val trackCornerRadius = CornerRadius(halfSliderHeightPx, halfSliderHeightPx) val indicatorCornerRadius = CornerRadius(indicatorRadiusPx, indicatorRadiusPx) Canvas(modifier.fillMaxSize()) { val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2) val trackPath = Path() trackPath.addRoundRect( RoundRect( left = -halfIconWidthPx, left = 0f, top = 0f, right = size.width + halfIconWidthPx, bottom = size.height, right = drawingState.totalWidth, bottom = drawingState.totalHeight, cornerRadius = trackCornerRadius, ) ) drawPath(path = trackPath, color = trackColor) val indicatorCornerRadius = CornerRadius(indicatorRadiusDp.toPx(), indicatorRadiusDp.toPx()) clipPath(trackPath) { val indicatorPath = Path() if (isRtl) { indicatorPath.addRoundRect( RoundRect( left = size.width - size.width * sliderState.coercedNormalizedValue - halfIconWidthPx, top = 0f, right = size.width + iconWidthPx, bottom = size.height, topLeftCornerRadius = indicatorCornerRadius, topRightCornerRadius = trackCornerRadius, bottomRightCornerRadius = trackCornerRadius, bottomLeftCornerRadius = indicatorCornerRadius, ) ) } else { indicatorPath.addRoundRect( RoundRect( left = -halfIconWidthPx, top = 0f, right = size.width * sliderState.coercedNormalizedValue + halfIconWidthPx, bottom = size.height, left = drawingState.indicatorLeft, top = drawingState.indicatorTop, right = drawingState.indicatorRight, bottom = drawingState.indicatorBottom, topLeftCornerRadius = trackCornerRadius, topRightCornerRadius = indicatorCornerRadius, bottomRightCornerRadius = indicatorCornerRadius, bottomLeftCornerRadius = trackCornerRadius, ) ) } drawPath(path = indicatorPath, color = indicatorColor) } } } /** Measures track components sizes and calls [onDrawingStateMeasured] when it's done. */ private class TrackMeasurePolicy( private val sliderState: SliderState, private val thumbSize: Int, private val isRtl: Boolean, private val onDrawingStateMeasured: (DrawingState) -> Unit, ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List<Measurable>, constraints: Constraints ): MeasureResult { // Slider adds a paddings to the Track to make spase for thumb val desiredWidth = constraints.maxWidth + thumbSize val desiredHeight = constraints.maxHeight val backgroundPlaceable: Placeable = measurables .fastFirst { it.layoutId == TrackComponent.Background } .measure(Constraints(desiredWidth, desiredWidth, desiredHeight, desiredHeight)) val iconPlaceable: Placeable? = measurables .fastFirstOrNull { it.layoutId == TrackComponent.Icon } ?.measure( Constraints( minWidth = desiredHeight, maxWidth = desiredHeight, minHeight = desiredHeight, maxHeight = desiredHeight, ) ) val iconSize = iconPlaceable?.width ?: 0 val labelMaxWidth = (desiredWidth - iconSize) / 2 val labelPlaceable: Placeable? = measurables .fastFirstOrNull { it.layoutId == TrackComponent.Label } ?.measure( Constraints( minWidth = 0, maxWidth = labelMaxWidth, minHeight = desiredHeight, maxHeight = desiredHeight, ) ) val drawingState = if (isRtl) { DrawingState( isRtl = true, totalWidth = desiredWidth.toFloat(), totalHeight = desiredHeight.toFloat(), indicatorLeft = (desiredWidth - iconSize) * (1 - sliderState.coercedNormalizedValue), indicatorTop = 0f, indicatorRight = desiredWidth.toFloat(), indicatorBottom = desiredHeight.toFloat(), iconWidth = iconSize.toFloat(), labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, ) } else { DrawingState( isRtl = false, totalWidth = desiredWidth.toFloat(), totalHeight = desiredHeight.toFloat(), indicatorLeft = 0f, indicatorTop = 0f, indicatorRight = iconSize + (desiredWidth - iconSize) * sliderState.coercedNormalizedValue, indicatorBottom = desiredHeight.toFloat(), iconWidth = iconSize.toFloat(), labelWidth = labelPlaceable?.width?.toFloat() ?: 0f, ) } onDrawingStateMeasured(drawingState) return layout(desiredWidth, desiredHeight) { backgroundPlaceable.placeRelative(0, 0, TrackComponent.Background.zIndex) iconPlaceable?.placeRelative(0, 0, TrackComponent.Icon.zIndex) labelPlaceable?.placeRelative(0, 0, TrackComponent.Label.zIndex) } } } private data class DrawingState( val isRtl: Boolean = false, val totalWidth: Float = 0f, val totalHeight: Float = 0f, val indicatorLeft: Float = 0f, val indicatorTop: Float = 0f, val indicatorRight: Float = 0f, val indicatorBottom: Float = 0f, val iconWidth: Float = 0f, val labelWidth: Float = 0f, ) private val DrawingState.isLabelOnTopOfIndicator: Boolean get() = labelWidth < indicatorRight - indicatorLeft - iconWidth /** [SliderState.value] normalized using [SliderState.valueRange]. The result belongs to [0, 1] */ private val SliderState.coercedNormalizedValue: Float get() { Loading @@ -268,17 +418,19 @@ private val SliderState.coercedNormalizedValue: Float * @param trackColor fills the track of the slider. This is a "background" of the slider * @param indicatorColor fills the slider from the start to the value * @param iconColor is the default icon color * @param labelColor is the default icon color * @param labelColorOnIndicator is the label color for when it's shown on top of the indicator * @param labelColorOnTrack is the label color for when it's shown on top of the track * @param disabledTrackColor is the [trackColor] when the PlatformSlider#enabled == false * @param disabledIndicatorColor is the [indicatorColor] when the PlatformSlider#enabled == false * @param disabledIconColor is the [iconColor] when the PlatformSlider#enabled == false * @param disabledLabelColor is the [labelColor] when the PlatformSlider#enabled == false * @param disabledLabelColor is the label color when the PlatformSlider#enabled == false */ data class PlatformSliderColors( val trackColor: Color, val indicatorColor: Color, val iconColor: Color, val labelColor: Color, val labelColorOnIndicator: Color, val labelColorOnTrack: Color, val disabledTrackColor: Color, val disabledIndicatorColor: Color, val disabledIconColor: Color, Loading @@ -300,10 +452,11 @@ object PlatformSliderDefaults { @Composable private fun lightThemePlatformSliderColors() = PlatformSliderColors( trackColor = MaterialTheme.colorScheme.tertiaryContainer, indicatorColor = LocalAndroidColorScheme.current.tertiaryFixedDim, iconColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColor = MaterialTheme.colorScheme.onTertiaryContainer, trackColor = LocalAndroidColorScheme.current.tertiaryFixedDim, indicatorColor = MaterialTheme.colorScheme.tertiary, iconColor = MaterialTheme.colorScheme.onTertiary, labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, Loading @@ -314,10 +467,11 @@ private fun lightThemePlatformSliderColors() = @Composable private fun darkThemePlatformSliderColors() = PlatformSliderColors( trackColor = MaterialTheme.colorScheme.onTertiary, indicatorColor = LocalAndroidColorScheme.current.onTertiaryFixedVariant, trackColor = MaterialTheme.colorScheme.tertiary, indicatorColor = MaterialTheme.colorScheme.tertiary, iconColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColor = MaterialTheme.colorScheme.onTertiaryContainer, labelColorOnIndicator = MaterialTheme.colorScheme.onTertiary, labelColorOnTrack = LocalAndroidColorScheme.current.onTertiaryFixed, disabledTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIndicatorColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledIconColor = MaterialTheme.colorScheme.outline, Loading @@ -329,3 +483,14 @@ private fun PlatformSliderColors.getTrackColor(isEnabled: Boolean): Color = private fun PlatformSliderColors.getIndicatorColor(isEnabled: Boolean): Color = if (isEnabled) indicatorColor else disabledIndicatorColor private fun PlatformSliderColors.getLabelColor( isEnabled: Boolean, isLabelOnTopOfTheIndicator: Boolean ): Color { return if (isEnabled) { if (isLabelOnTopOfTheIndicator) labelColorOnIndicator else labelColorOnTrack } else { disabledLabelColor } }