Compose Shape Slider

本文详细介绍了如何在Compose中实现自定义Material3风格的Slider,包括Slider的属性、默认实现与难点,以及如何通过ShapeDrawable和Modifier实现圆角效果,涉及Track、Thumb的绘制和颜色定制。
摘要由CSDN通过智能技术生成


Compose 提供了 Slider 替代 View中的ProgressBar. ProgressBar 可以通过自定义 drawable 实现丰富的外观效果, 特别是是通过ShapeDrawable 实现圆角效果。

Compose 中默认提供的Slider 是Materail3 风格的,要想实现 Android View中的自定义风格 需要实现自己的Track 和Thumb Compose 函数。

一、Compose Materail3 Slider

Slider的属性

Slider是Android Jetpack Compose中的一个控件,用于实现滑动条的功能。

它具有以下常用的属性:

  1. value:滑动条的当前值。可以使用value参数来设置初始值,并通过onValueChange参数监听值的变化。

  2. onValueChange:滑动条值变化时的回调函数。可以在这个回调函数中处理滑动条值的更新逻辑。

  3. valueRange:滑动条的值范围。通过valueRange参数可以设置滑动条的最小值和最大值,如valueRange = 0f…100f。

  4. steps:滑动条的步长。可以使用steps参数设置滑动条每次滑动时的增量。

  5. modifier:用于修改滑动条的外观和行为的修饰符。例如,可以使用Modifier.height(48.dp)来设置滑动条的高度。

  6. colors:用于自定义Slider的颜色。可以设置滑块、激活轨道和非激活轨道的颜色。

Slider 的实现 SliderImpl

通过试用Compose Slider 以后发现默认的Slider 无法实现圆角效果。看了下Google的默认实现,

  1. draggableState gestureEndAction press drag 这几个Modifier 修饰符 完成触摸操作,进度条计算,刻度计算等。
  2. 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.TRACK)) { track(sliderPositions) }
        },
        modifier = modifier
            .minimumTouchTargetSize()
            .requiredSizeIn(
                minWidth = SliderTokens.HandleWidth,
                minHeight = SliderTokens.HandleHeight
            )
            .sliderSemantics(
                value,
                enabled,
                onValueChange,
                onValueChangeFinished,
                valueRange,
                steps
            )
            .focusable(enabled, interactionSource)
            .then(press)
            .then(drag)
    ) { measurables, constraints ->

        val thumbPlaceable = measurables.first {
            it.layoutId == SliderComponents.THUMB
        }.measure(constraints)

        val maxTrackWidth = constraints.maxWidth - thumbPlaceable.width
        val trackPlaceable = measurables.first {
            it.layoutId == SliderComponents.TRACK
        }.measure(
            constraints.copy(
                minWidth = 0,
                maxWidth = maxTrackWidth,
                minHeight = 0
            )
        )

        val sliderWidth = thumbPlaceable.width + trackPlaceable.width
        val sliderHeight = max(trackPlaceable.height, thumbPlaceable.height)

        thumbWidth.value = thumbPlaceable.width.toFloat()
        totalWidth.value = sliderWidth

        val trackOffsetX = thumbPlaceable.width / 2
        val thumbOffsetX = ((trackPlaceable.width) * positionFraction).roundToInt()
        val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
        val thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2

        layout(
            sliderWidth,
            sliderHeight
        ) {
            trackPlaceable.placeRelative(
                trackOffsetX,
                trackOffsetY
            )
            thumbPlaceable.placeRelative(
                thumbOffsetX,
                thumbOffsetY
            )
        }
    }
}

material3 提供了 Track和Thumb 的默认实现。

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Slider(
) {
    Slider(
        ...
        
        thumb = remember(interactionSource, colors, enabled) { {
            SliderDefaults.Thumb(
                interactionSource = interactionSource,
                colors = colors,
                enabled = enabled
            )
        } },
        track = remember(colors, enabled) { { sliderPositions ->
            SliderDefaults.Track(
                colors = colors,
                enabled = enabled,
                sliderPositions = sliderPositions
            )
        } }
    )
}

Track的默认实现

Track的默认实现中,是通过 drawLine 函数绘制的Track。通过drawPoints 绘制刻度。

    fun Track(
        sliderPositions: SliderPositions,
        modifier: Modifier = Modifier,
        colors: SliderColors = colors(),
        enabled: Boolean = true,
    ) {
        ...
        
        Canvas(modifier
            .fillMaxWidth()
            .height(TrackHeight)
        ) {

            drawLine(
                inactiveTrackColor.value,
                sliderStart,
                sliderEnd,
                trackStrokeWidth,
                StrokeCap.Round
            )


            drawLine(
                activeTrackColor.value,
                sliderValueStart,
                sliderValueEnd,
                trackStrokeWidth,
                StrokeCap.Round
            )
            
            sliderPositions.tickFractions.groupBy {
                it > sliderPositions.positionFraction ||
                    it < 0f
            }.forEach { (outsideFraction, list) ->
                    drawPoints(
                        list.map {
                            Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
                        },
                        PointMode.Points,
                        (if (outsideFraction) inactiveTickColor else activeTickColor).value,
                        tickSize,
                        StrokeCap.Round
                    )
                }
        }
    }
}

drawLine绘制 Cap

drawLine 函数的绘制Line时 支持三种Cap , 因此 Compose 的默认实现是无法通过Shape 自定义实现我们想要的效果。

/**
 * Begin and end contours with a flat edge and no extension.
 */
val Butt = StrokeCap(0)

/**
 * Begin and end contours with a semi-circle extension.
 */
val Round = StrokeCap(1)

/**
 * Begin and end contours with a half square extension. This is
 * similar to extending each contour by half the stroke width (as
 * given by [Paint.strokeWidth]).
 */
val Square = StrokeCap(2)

请添加图片描述

Thumb的实现方式

Thumb 是通过 Modifier shadow 和 background 方式,可以实现圆角。

fun Thumb(
    interactionSource: MutableInteractionSource,
    modifier: Modifier = Modifier,
    colors: SliderColors = colors(),
    enabled: Boolean = true,
    thumbSize: DpSize = ThumbSize
) {
    val shape = SliderTokens.HandleShape.toShape()

    Spacer(
        modifier
            .size(thumbSize)
            .indication(
                interactionSource = interactionSource,
                indication = rememberRipple(
                    bounded = false,
                    radius = SliderTokens.StateLayerSize / 2
                )
            )
            .hoverable(interactionSource = interactionSource)
            .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
            .background(colors.thumbColor(enabled).value, shape)
    )
}

SliderColors

Materail3 Slider 各个组件通过SliderColors 配置颜色。 区分 enabled 状态, enabled 为false, 不可滑动。

class SliderColors internal constructor(
    private val thumbColor: Color,
    private val activeTrackColor: Color,
    private val activeTickColor: Color,
    private val inactiveTrackColor: Color,
    private val inactiveTickColor: Color,
    private val disabledThumbColor: Color,
    private val disabledActiveTrackColor: Color,
    private val disabledActiveTickColor: Color,
    private val disabledInactiveTrackColor: Color,
    private val disabledInactiveTickColor: Color
) {

    @Composable
    internal fun thumbColor(enabled: Boolean): State<Color> {
        return rememberUpdatedState(if (enabled) thumbColor else disabledThumbColor)
    }

    @Composable
    internal fun trackColor(enabled: Boolean, active: Boolean): State<Color> {
        return rememberUpdatedState(
            if (enabled) {
                if (active) activeTrackColor else inactiveTrackColor
            } else {
                if (active) disabledActiveTrackColor else disabledInactiveTrackColor
            }
        )
    }

    @Composable
    internal fun tickColor(enabled: Boolean, active: Boolean): State<Color> {
        return rememberUpdatedState(
            if (enabled) {
                if (active) activeTickColor else inactiveTickColor
            } else {
                if (active) disabledActiveTickColor else disabledInactiveTickColor
            }
        )
    }

SliderDefaults 提供Materail3 的默认配置,很多变量被通过 internelobject 被限定访问权限。无法修改实现自定义风格。why?

object SliderDefaults {

    @Composable
    fun colors(
        thumbColor: Color = SliderTokens.HandleColor.toColor(),
        activeTrackColor: Color = SliderTokens.ActiveTrackColor.toColor(),
        activeTickColor: Color = SliderTokens.TickMarksActiveContainerColor
            .toColor()
            .copy(alpha = SliderTokens.TickMarksActiveContainerOpacity),
        inactiveTrackColor: Color = SliderTokens.InactiveTrackColor.toColor(),
        inactiveTickColor: Color = SliderTokens.TickMarksInactiveContainerColor.toColor()
            .copy(alpha = SliderTokens.TickMarksInactiveContainerOpacity),
        disabledThumbColor: Color = SliderTokens.DisabledHandleColor
            .toColor()
            .copy(alpha = SliderTokens.DisabledHandleOpacity)
            .compositeOver(MaterialTheme.colorScheme.surface),
        disabledActiveTrackColor: Color =
            SliderTokens.DisabledActiveTrackColor
                .toColor()
                .copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
        disabledActiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor
            .toColor()
            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity),
        disabledInactiveTrackColor: Color =
            SliderTokens.DisabledInactiveTrackColor
                .toColor()
                .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),

        disabledInactiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor.toColor()
            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity)
    ): SliderColors = SliderColors(
        thumbColor = thumbColor,
        activeTrackColor = activeTrackColor,
        activeTickColor = activeTickColor,
        inactiveTrackColor = inactiveTrackColor,
        inactiveTickColor = inactiveTickColor,
        disabledThumbColor = disabledThumbColor,
        disabledActiveTrackColor = disabledActiveTrackColor,
        disabledActiveTickColor = disabledActiveTickColor,
        disabledInactiveTrackColor = disabledInactiveTrackColor,
        disabledInactiveTickColor = disabledInactiveTickColor
    )

二 Shape Slider

通过分析Slider 的实现方式,

  1. 可以通过 Modifier 修饰符实现圆角效果,甚至Compose Modifier 支持的效果都可以实现
  2. 可以通过Canvas 函数绘制UI,就像View 中的Canvas 绘制一样,但是圆角的绘制过程比较复杂。
  3. Compose Slider 的滑动操作和 UI 绘制是分开的, 支持自定义实现 Track和Slider。

因此我们通过 Modifier 修饰符 的方式实现自定义 Track和Slider, 通过Modifier 的shadow 和background 实现我们想要的圆角效果。而滑动操作计算过程比较复杂,直接使用默认实现。

ShapeTrack 的绘制

Track的绘制分为三个部分,

  1. inactiveTrack 的绘制, 如果使用Shape 修饰符实现,这一快修改为 WholeTrack 的绘制。
  2. activeTrack 的绘制,在FullTrack 上覆盖绘制即可。
  3. tick 的绘制,复用Compose slider 的默认实现。

定义Track , 在layout 函数中,放置两个 Sapcer 分别代表 WholeTrack 和ActivieTrack , 然后使用相应的Modifier 修饰符修饰。

在这里使用了Modifier 的composed 函数,将自定义的 Shape Modifier 应用在 Spacker 上面。

Declare a just-in-time composition of a Modifier that will be composed for each element it modifies. composed may be used to implement stateful modifiers that have instance-specific state for each modified element, allowing the same Modifier instance to be safely reused for multiple elements while maintaining element-specific state.
If inspectorInfo is specified this modifier will be visible to tools during development. Specify the name and arguments of the original modifier.

声明一个 Modifier 的composition,该组合将针对它修改的每个元素进行组合。 composed 可用于实现有状态修饰符,该修饰符为每个被修改的元素具有特定于实例的状态,允许相同的修饰符实例安全地重用于多个元素,同时维护特定于元素的状态。如果指定了spectorInfo,则该修饰符将在开发过程中对工具可见。指定原始修饰符的名称和参数。

Layout布局
Layout(
    {
        Spacer(
            modifier = Modifier
                .layoutId(trackFullID)
                .fillMaxWidth()
                .composed {
                    modifierTrack
                }
        )

        Spacer(
            modifier = Modifier
                .layoutId(trackActiveID)
                .fillMaxWidth()
                .composed {
                    modifierActive
                }
        )
    },
measure测量

根据sliderPositions 确定绘制ActiviTrack 的长度, 测量后layout

) { measurables, constraints ->

    val fullPlaceable = measurables.first { it.layoutId == trackFullID }.measure(
        constraints.copy(
            minWidth = 0,
            maxWidth = constraints.maxWidth + offset.toPx().toInt(),
            minHeight = 0
        )
    )

    val maxTrackWidth = constraints.maxWidth
    val activePlaceable = measurables.first { it.layoutId == trackActiveID }.measure(
        constraints.copy(
            minWidth = 0,
            maxWidth = (maxTrackWidth * sliderPositions.positionFraction).toInt() + offset.toPx().toInt(),
            minHeight = 0
        )
    )

    val sliderWidth = max(fullPlaceable.width, activePlaceable.width)
    val sliderHeight = max(activePlaceable.height, fullPlaceable.height)

    val activeOffsetX = 0
    val activeOffsetY = (max(activePlaceable.height, fullPlaceable.height) - min(activePlaceable.height, fullPlaceable.height)) / 2

    val fullOffsetX = 0
    val fullOffsetY = (max(activePlaceable.height, fullPlaceable.height) - min(activePlaceable.height, fullPlaceable.height)) / 2

    layout(sliderWidth, sliderHeight) {

        fullPlaceable.placeRelative(fullOffsetX, fullOffsetY)
        activePlaceable.placeRelative(activeOffsetX, activeOffsetY)
    }
}
Canvas 绘制刻度
Canvas(
    modifier
        .fillMaxWidth()
        .height(tickHeight) ){
    val isRtl = layoutDirection == LayoutDirection.Rtl
    val sliderLeft = Offset(0f, center.y)
    val sliderRight = Offset(size.width, center.y)
    val sliderStart = if (isRtl) sliderRight else sliderLeft
    val sliderEnd = if (isRtl) sliderLeft else sliderRight

    sliderPositions.tickFractions.groupBy {
        it > sliderPositions.positionFraction || it < 0f
    }.forEach { (outsideFraction, list) ->
        drawPoints(
            list.map {
                Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
            },
            PointMode.Points,
            (if (outsideFraction) inactiveTickColor else activeTickColor).value,
            tickSize.toPx(),
            StrokeCap.Round
        )
    }
}

ShapeThumb

修改 Spacer 的 Modifier的实现,使用composed 函数 应用自定义 Modifier的修改

@Composable
@ExperimentalMaterial3Api
fun ShapeThumb(
    interactionSource: MutableInteractionSource,
    modifier: Modifier = Modifier,
    modifierThumb: Modifier,
) {
    Spacer(
        modifier
            ...
            .composed {
                modifierThumb
            }
    )
}

ShapeSlider的最终实现

fun ShapeSlider(
    value: Float,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
    /*@IntRange(from = 0)*/
    steps: Int = 0,
    colors: AutraSliderColors,
    onValueChange: (Float) -> Unit,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },

    tickHeight: Dp,
    trackOffset: Dp = 0.dp,
    tickSize:Dp = 2.dp,

    modifierShapeTrack: Modifier = Modifier,
    modifierShapeActive: Modifier = Modifier,
    modifierThumb: Modifier = Modifier,
) 

Compose Slider 的默认参数

value: Float,
modifier: Modifier = Modifier,
enabled: Boolean = true,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
/*@IntRange(from = 0)*/
steps: Int = 0,
colors: AutraSliderColors,
onValueChange: (Float) -> Unit,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
1. tickHeight: Dp,                   tick 的高度,影响tick 在Track 垂直方向的偏移
2. trackOffset: Dp = 0.dp,           activie 的偏移量,参考图,active track 超出thumb 部分
3. tickSize:Dp = 2.dp,               tick 点的大小
1. modifierShapeTrack: Modifier = Modifier,      WholeTrack 的Modifier
2. modifierShapeActive: Modifier = Modifier,     ActiveTrack 的Modifier
3. modifierThumb: Modifier = Modifier,           Thumb Modiffier

最终效果

ShapeSlider(
    sliderPosition,
    Modifier,
    enabled = true,
    colors = colors,
    onValueChange = { sliderPosition = it
        Log.d("ShapeSlider", "ShapeSlider onValueChange $it")
    },
    tickHeight = 82.dp,
    trackOffset = 10.dp,
    tickSize = 5.dp,
    modifierShapeTrack = Modifier
        .shadow(
            elevation = 24.dp,
            spotColor = Color(0x291E293B),
            ambientColor = Color(0x291E293B)
        )
        .shadow(
            elevation = 2.dp,
            spotColor = Color(0x0A1E293B),
            ambientColor = Color(0x0A1E293B)
        )
        .height(82.dp)
        .background(
            color = colors.trackColor(true, false).value,
            shape = RoundedCornerShape(size = 12.dp)
        ),
    modifierShapeActive = Modifier
        .shadow(
            elevation = 2.dp,
            spotColor = Color(0x1F1E293B),
            ambientColor = Color(0x1F1E293B)
        )
        .height(82.dp)
        .background(
            color = colors.trackColor(true, true).value,
            shape = RoundedCornerShape(size = 8.dp)
        ),
    modifierThumb = Modifier
        .size(DpSize(8.dp, 50.dp))
        .shadow(
            elevation = 2.dp,
            spotColor = Color(0x1F1E293B),
            ambientColor = Color(0x1F1E293B)
        )
        .width(8.dp)
        .height(50.dp)
        .background(
            color = colors.thumbColor(true).value,
            shape = RoundedCornerShape(size = 8.dp)
        )
)

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值