效果图
免责声明
由于代码测试少,没封装,经不起考验,仅供学习。还有一个是效果是我借鉴的某篇文章的,但是我找不到原文了,抱歉。
功能思路
本身也没啥好说的,东西太简单了,就当写给还没接触过的同学吧,有经验的同学完全可以跳过整篇文章或者学习一下compose中如何实现。
我说下我的思路,有些同学反应快可以一步到位。首先需要将功能分解,千里之行,始于足下。
- 静态的底部圆环
- 动态的上方圆弧:圆环的stroke略大于底部圆环
- 文字的绘制:百分比和“出勤率”是以圆心的水平线为分界线
- 接着考虑再考虑百分比文字和进度的动画效果
绘制内部圆环和外部圆弧
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我当时想了挺久到底是个啥,其实就是字面意思,让你指定:区域的左上角在哪里
绘制文字
目前drawText有四种方法,主要是两种,如下图12行为第一种方法,34行为第二种方法
第一种和第二种区别不大,主要是颜色是传递Brush还是Color。
对着下方的一张图(drawText方法属性)和一份代码看:如果是使用的第二种方法,需要传递的第一个参数为TextMeasurer,也就是val textMeasure = rememberTextMeasurer(),没有后面的measure(......)。使用这种方法有个地方需要注意,如果你需要设置文本的style属性,并且属性会导致文本的大小出现变化,比如加粗,改变文本的sp,这个地方有个坑就是你在drawText()的时候,里面有个参数需要你设置style,当你设置了改变文字的style后,你以为没任何问题,结果你会发现写出来的UI和你的期望有偏差,这里需要另外调用textMeasure.measure(....),里面再设置一遍你的style,比较的麻烦。所以我还是推荐我下面这种写法,如果没明白我在说什么,可以自己在编辑器里对着drawText属性看一下,光看不做假把式。
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太厉害啦。下课