Jetpack Compose 学习笔记: 尝试自定义一个Compose版的Seekbar(滑动进度条)

1,439 阅读1分钟

Jetpack Compose 学习笔记: 尝试自定义一个Compose版的Seekbar(滑动进度条)

效果图

iShot_2022-08-01_19.51.45.png

组件的构成

  • 一个显示百分比的文本
  • 两根重叠的直线: 一根在底部作为总进度, 一根在顶部作为当前进度
  • 一个进度锚点: 用于响应滑动以及指示当前进度, 滑动时‘平滑’地变大

第一步的文本可以直接使用Text, 后两步使用Canvas绘制, 但自定义绘制也只用到drawLinedrawCircle, 整个组件并不复杂

直接上代码

    @Composable
    fun CustomSeekbar(
        modifier: Modifier,
        onProgressChanged: (progress: Float) -> Unit
    ) {
        // 当前进度,范围0-1之间, 初始为0
        var progress by remember { mutableStateOf(0f) }
        // bar是否被按下
        var barPressed by remember { mutableStateOf(false) }
        // 锚点的半径, 根据barPressed的状态'平滑'地改变自身的大小
        val radius by animateFloatAsState(if (barPressed) 30f else 20f)
        Row(
            modifier = modifier,
            verticalAlignment = Alignment.CenterVertically
        ) {
            // 进度的文本
            Text(text = (progress * 100).toInt().toString(), Modifier.width(30.dp))
            Canvas(
                modifier = Modifier
                    .height(30.dp)
                    .fillMaxWidth()
                    .weight(1f)
                    .padding(10.dp)
                    .pointerInput(Unit) {
                        detectDragGestures( // 响应滑动事件
                            onDragStart = { barPressed = true },
                            onDragCancel = { barPressed = false },
                            onDragEnd = {
                                // 滑动结束时, 恢复锚点大小,并回调onProgressChanged函数
                                barPressed = false
                                onProgressChanged.invoke(progress)
                            },
                            onDrag = { change, dragAmount ->
                                // 滑动过程中, 实时刷新progress的值(注意左右边界的问题),
                                // 此值一旦改变, 整个Seekbar就会重组(刷新)
                                progress = if (change.position.x < 0) {
                                    0f
                                } else if (change.position.x > size.width) {
                                    1f
                                } else {
                                    (change.position.x / this.size.width)
                                }
                            })
                    }
                    .pointerInput(Unit) {
                        // 响应点击事件, 直接跳到该进度处
                        detectTapGestures(onTap = {
                            progress = (it.x / size.width)
                            barPressed = false
                        })
                    },
                onDraw = {
                    // 底部灰色线条
                    drawLine(
                        color = Color.Gray.copy(alpha = 0.5f),
                        start = Offset(0f, size.height / 2),
                        end = Offset(size.width, size.height / 2),
                        strokeWidth = 8f
                    )
                    // 顶部蓝色线条
                    drawLine(
                        color = Color.Blue,
                        start = Offset(0f, size.height / 2),
                        end = Offset(size.width * progress, size.height / 2),
                        strokeWidth = 12f
                    )
                    // 锚点
                    drawCircle(
                        color = Color.Blue,
                        radius = radius,
                        center = Offset(size.width * progress, size.height / 2)
                    )
                })
        }
    }

    @Preview(showBackground = true)
    @Composable
    fun previewCustomSeekbar() {
        // 预览该composable组件
        CustomSeekbar(
            modifier = Modifier
                .fillMaxWidth()
                .background(color = Color.Gray.copy(alpha = 0.1f))
                .padding(horizontal = 20.dp),
        ) {
            Log.d("TAG", "seekbar: onProgressChanged=$it")
        }
    }

运行测试

直接复制以上代码, 添加到其他composable组件内(如column), 即可运行看到效果

iShot_2022-08-01_20.20.41.png