Compose Shape Slider
Compose 提供了 Slider 替代 View中的ProgressBar. ProgressBar 可以通过自定义 drawable 实现丰富的外观效果, 特别是是通过ShapeDrawable 实现圆角效果。
Compose 中默认提供的Slider 是Materail3 风格的,要想实现 Android View中的自定义风格 需要实现自己的Track 和Thumb Compose 函数。
一、Compose Materail3 Slider
Slider的属性
Slider是Android Jetpack Compose中的一个控件,用于实现滑动条的功能。
它具有以下常用的属性:
value:滑动条的当前值。可以使用value参数来设置初始值,并通过onValueChange参数监听值的变化。
onValueChange:滑动条值变化时的回调函数。可以在这个回调函数中处理滑动条值的更新逻辑。
valueRange:滑动条的值范围。通过valueRange参数可以设置滑动条的最小值和最大值,如valueRange = 0f…100f。
steps:滑动条的步长。可以使用steps参数设置滑动条每次滑动时的增量。
modifier:用于修改滑动条的外观和行为的修饰符。例如,可以使用Modifier.height(48.dp)来设置滑动条的高度。
colors:用于自定义Slider的颜色。可以设置滑块、激活轨道和非激活轨道的颜色。
Slider 的实现 SliderImpl
通过试用Compose Slider 以后发现默认的Slider 无法实现圆角效果。看了下Google的默认实现,
- draggableState gestureEndAction press drag 这几个Modifier 修饰符 完成触摸操作,进度条计算,刻度计算等。
- Layout 函数完成 Track和Thumb 的宽高和位置的计算。
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SliderImpl(
modifier: Modifier,
enabled: Boolean,
interactionSource: MutableInteractionSource,
onValueChange: (Float) -> Unit,
onValueChangeFinished: (() -> Unit)?,
steps: Int,
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
thumb: @Composable (SliderPositions) -> Unit,
track: @Composable (SliderPositions) -> Unit
) {
val onValueChangeState = rememberUpdatedState<(Float) -> Unit> {
if (it != value) {
onValueChange(it)
}
}
val tickFractions = remember(steps) {
stepsToTickFractions(steps)
}
val thumbWidth = remember {
mutableStateOf(ThumbWidth.value) }
val totalWidth = remember {
mutableStateOf(0) }
fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) =
scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val rawOffset = remember {
mutableStateOf(scaleToOffset(0f, 0f, value)) }
val pressOffset = remember {
mutableStateOf(0f) }
val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
val positionFraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced)
val sliderPositions = remember {
SliderPositions(positionFraction, tickFractions) }
sliderPositions.positionFraction = positionFraction
sliderPositions.tickFractions = tickFractions
val draggableState = remember(valueRange) {
SliderDraggableState {
val maxPx = max(totalWidth.value - thumbWidth.value / 2, 0f)
val minPx = min(thumbWidth.value / 2, maxPx)
rawOffset.value = (rawOffset.value + it + pressOffset.value)
pressOffset.value = 0f
val offsetInTrack = snapValueToTick(rawOffset.value, tickFractions, minPx, maxPx)
onValueChangeState.value.invoke(scaleToUserValue(minPx, maxPx, offsetInTrack))
}
}
val gestureEndAction = rememberUpdatedState {
if (!draggableState.isDragging) {
// check isDragging in case the change is still in progress (touch -> drag case)
onValueChangeFinished?.invoke()
}
}
val press = Modifier.sliderTapModifier(
draggableState,
interactionSource,
totalWidth.value,
isRtl,
rawOffset,
gestureEndAction,
pressOffset,
enabled
)
val drag = Modifier.draggable(
orientation = Orientation.Horizontal,
reverseDirection = isRtl,
enabled = enabled,
interactionSource = interactionSource,
onDragStopped = {
_ -> gestureEndAction.value.invoke() },
startDragImmediately = draggableState.isDragging,
state = draggableState
)
Layout(
{
Box(modifier = Modifier.layoutId(SliderComponents.THUMB)) {
thumb(sliderPositions) }
Box(modifier = Modifier.layoutId(SliderComponents