用jetpack-compose写个进度条

1,257 阅读4分钟

效果图

效果图

免责声明

由于代码测试少,没封装,经不起考验,仅供学习。还有一个是效果是我借鉴的某篇文章的,但是我找不到原文了,抱歉。

MMTJ0QH_%H87GB{QXXWPQI3.jpg

功能思路

本身也没啥好说的,东西太简单了,就当写给还没接触过的同学吧,有经验的同学完全可以跳过整篇文章或者学习一下compose中如何实现。
我说下我的思路,有些同学反应快可以一步到位。首先需要将功能分解,千里之行,始于足下。

QQ截图20230519170302.png

  1. 静态的底部圆环
  2. 动态的上方圆弧:圆环的stroke略大于底部圆环
  3. 文字的绘制:百分比和“出勤率”是以圆心的水平线为分界线
  4. 接着考虑再考虑百分比文字和进度的动画效果

绘制内部圆环和外部圆弧

Canvas(modifier = Modifier.size(300.dp), onDraw = {
    val innerStrokeWidth = 10.dp.toPx()
    val radius = 120.dp.toPx()
    val outStrokeWidth = 17.dp.toPx()
    val canvasWidth = size.width
    val canvasHeight = size.height
    //内部圆
    drawCircle(
        Color(222, 228, 246),
        radius = radius,
        center = Offset(canvasWidth / 2, canvasHeight / 2),
        style = Stroke(innerStrokeWidth)
    )
    //圆弧进度
    drawArc(
        Color(46, 120, 249),
        startAngle = -90f,
        sweepAngle = 120f,
        useCenter = false,
        size = Size(radius * 2, radius * 2),
        style = Stroke(outStrokeWidth, cap = StrokeCap.Round),
        topLeft = Offset(center.x - radius, center.y - radius)
    )
})

定义的变量从名字应该比较容易看出来,需要注意的是.toPx()和.toDp()等这种便捷方法只有在DrawScope区域里也就是onDraw = { 区域 } 里才能使用。 drawCircle中Offset用于确定圆的圆心,size也是也是DrawScope区域内的属性,可获取Canvas尺寸等信息,比如size.width获取的就是设置画布的宽度为300dp(转成像素)。
还一个重点是drawArc中的size和topLeft属性。我们需要画的是圆弧,size用于确定绘制的范围,所以自然就可以确定size是一个正方形。
倒是这个topLeft我当时想了挺久到底是个啥,其实就是字面意思,让你指定:区域的左上角在哪里

image.png

有人能推荐下免费的好用的画图工具吗,自带的用着不舒服啊。

绘制文字

目前drawText有四种方法,主要是两种,如下图12行为第一种方法,34行为第二种方法

image.png

第一种和第二种区别不大,主要是颜色是传递Brush还是Color。
对着下方的一张图(drawText方法属性)和一份代码看:如果是使用的第二种方法,需要传递的第一个参数为TextMeasurer,也就是val textMeasure = rememberTextMeasurer(),没有后面的measure(......)。使用这种方法有个地方需要注意,如果你需要设置文本的style属性,并且属性会导致文本的大小出现变化,比如加粗,改变文本的sp,这个地方有个坑就是你在drawText()的时候,里面有个参数需要你设置style,当你设置了改变文字的style后,你以为没任何问题,结果你会发现写出来的UI和你的期望有偏差,这里需要另外调用textMeasure.measure(....),里面再设置一遍你的style,比较的麻烦。所以我还是推荐我下面这种写法,如果没明白我在说什么,可以自己在编辑器里对着drawText属性看一下,光看不做假把式。

image.png


val textPercent = "60%"
//测量文字
val textPercentLayResult = rememberTextMeasurer().measure(
    text = AnnotatedString(textPercent),
    style = TextStyle(
        color = Color(96, 98, 172),
        fontSize = 30.sp,
        fontWeight = FontWeight.Bold
    )
)
Canvas(modifier = Modifier.size(300.dp), onDraw = {

    val canvasWidth = size.width
    val canvasHeight = size.height

    val textPercentWidth = textPercentLayResult.size.width
    val textPercentHeight = textPercentLayResult.size.height
    //百分比文字
    drawText(
        textLayoutResult = textPercentLayResult,
        topLeft = Offset(
            canvasWidth / 2 - textPercentWidth / 2,
            canvasHeight / 2 - textPercentHeight
        ),
    )
})

topLeft的属性已经很明显了,刚才讲圆弧的时候讲过了,就是第一个文字的左上方,其他的变量名也比较容易知道用途

动画效果

在compose里的动画效果(不止动画)相比原生真的是容易太多了

//sweepState就是外部传进来的百分比,比如最大是100,  
当前是60,那么sweepState就是0.6,目标值就是360*0.6=60,  
所以扫过的角度animAngle就是60度。
val animAngle = animateFloatAsState(
    targetValue = sweepState.value * 360,
    animationSpec = tween(1000)
)
//0.6 * 100 = 60就是我们显示的百分比
val animPercent = animateIntAsState(
    targetValue = (sweepState.value * 100).toInt(),
    animationSpec = tween(1000)
)
//赋值给textPercent,传递到要测量的文本中
val textPercent = "${animPercent.value}%"

val textPercentLayResult = rememberTextMeasurer().measure(
    text = AnnotatedString(textPercent),
    。。。
)

drawArc(
    。。。
    sweepAngle = animAngle.value,
    。。。
)

animateFloatAsState的第二个参数是配置动画的属性,有兴趣可以看下别人的文章了解下有哪些动画配置属性,反正我是记不清。 出勤率的绘制没啥好说,就单纯是个文本比较简单,同“百分比”文字

源码

@Composable
fun Progress() {
    val sweepState = remember {
        mutableStateOf(0f)
    }
    val max = 100f
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Spacer(modifier = Modifier.height(20.dp))
        ProgressBarView(sweepState)
        Spacer(modifier = Modifier.height(20.dp))
        Button(onClick = {
            sweepState.value = Random.nextInt(1 until 99) / max
        }) {
            Text(text = "按钮")
        }
    }
}

@OptIn(ExperimentalTextApi::class)
@Composable
private fun ProgressBarView(sweepState: MutableState<Float>) {
    val animAngle = animateFloatAsState(
        targetValue = sweepState.value * 360,
        animationSpec = tween(1000)
    )
    val animPercent = animateIntAsState(
        targetValue = (sweepState.value * 100).toInt(),
        animationSpec = tween(1000)
    )
    val textPercent = "${animPercent.value}%"
    val textPercentLayResult = rememberTextMeasurer().measure(
        text = AnnotatedString(textPercent),
        style = TextStyle(
            color = Color(96, 98, 172),
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold
        )
    )
    val textDesc = "出勤率"
    val textDescLayoutResult = rememberTextMeasurer().measure(
        AnnotatedString(textDesc),
        TextStyle(color = Color(178, 193, 209))
    )
    Canvas(modifier = Modifier.size(300.dp), onDraw = {
        val innerStrokeWidth = 10.dp.toPx()
        val radius = 120.dp.toPx()
        val outStrokeWidth = 17.dp.toPx()
        val canvasWidth = size.width
        val canvasHeight = size.height
        //内部圆
        drawCircle(
            Color(222, 228, 246),
            radius = radius,
            center = Offset(canvasWidth / 2, canvasHeight / 2),
            style = Stroke(innerStrokeWidth)
        )
        //圆弧进度
        drawArc(
            Color(46, 120, 249),
            startAngle = -90f,
            sweepAngle = animAngle.value,
            useCenter = false,
            size = Size(radius * 2, radius * 2),
            style = Stroke(outStrokeWidth, cap = StrokeCap.Round),
            topLeft = Offset(center.x - radius, center.y - radius)
        )
        val textPercentWidth = textPercentLayResult.size.width
        val textPercentHeight = textPercentLayResult.size.height
        //百分比文字
        drawText(
            textLayoutResult = textPercentLayResult,
            topLeft = Offset(
                canvasWidth / 2 - textPercentWidth / 2,
                canvasHeight / 2 - textPercentHeight
            ),
        )

        val textDescWidth = textDescLayoutResult.size.width
        val textDescHeight = textDescLayoutResult.size.height //用不着
        //出勤率
        drawText(
            textLayoutResult = textDescLayoutResult,
            topLeft = Offset(
                canvasWidth / 2 - textDescWidth / 2,
                canvasHeight / 2
            ),
        )
    })

}

最后

好像看起来实现的代码量好多一样,其实如果不换行的话真没几行,相比原生的实现我只想说:我们compose太厉害啦。下课