Compose 利用Canvas绘制圆形进度条、线性进度条、饼图、柱状图

229 阅读1分钟

Compose 利用Canvas绘制圆形进度条、线性进度条、饼图、柱状图

绘制圆形进度条

进度背景、进度、标题文字、进度文字

Screenshot_20240206_095911.png
@Composable
@OptIn(ExperimentalTextApi::class)
private fun CircleProgress(
    title: String = "完成率",//标题
    titleSize: Float = 18f,//标题大小
    progressSize: Float = 16f,//进度字体大小
    progress: Float = 0f,//进度
    textColor: Color = Color.White,//字体颜色
    bgColor: Color = Color(0xFF1A5476),//背景颜色
    progressColor: Color = Color(0xFF1CCDCB),//进度颜色
    strokeWidth: Dp = 60.dp,//进度条宽度
    spaceWidth: Dp = 200.dp//画布大小
) {
    Box(
        Modifier
            .wrapContentSize()
            .background(Color(0xFF1D405D))
    ) {

        val measurer = rememberTextMeasurer()
        Canvas(modifier = Modifier.size(spaceWidth), onDraw = {
            val radius = (size.width.minus(strokeWidth.value)).div(2)
            val space = size.width
            //背景
            drawCircle(
                bgColor, radius = radius, style = Stroke(width = strokeWidth.value)
            )
            //进度
            drawArc(
                color = progressColor,
                topLeft = Offset(strokeWidth.value / 2, strokeWidth.value / 2),
                size = Size(space - strokeWidth.value, space - strokeWidth.value),
                useCenter = false,
                startAngle = 0f,
                sweepAngle = if (progress > 100f) 360f else progress.times(360f.div(100f)),
                style = Stroke(width = strokeWidth.value, cap = StrokeCap.Round)
            )
            var resultTitle = measurer.measure(
                AnnotatedString(title), style = TextStyle(
                    textColor, fontSize = TextUnit(
                        titleSize, TextUnitType.Sp
                    )
                )
            )
            drawText(
                resultTitle, topLeft = Offset(
                    (space - resultTitle.size.width).div(2),
                    space.div(2).minus(resultTitle.size.height).minus(4.dp.value)
                )
            )
            val np = DecimalFormat("0.00")
            val str = np.format(progress)
            var result = measurer.measure(
                AnnotatedString("$str%"), style = TextStyle(
                    textColor, fontSize = TextUnit(
                        progressSize, TextUnitType.Sp
                    )
                )
            )
            drawText(
                result, topLeft = Offset(
                    (space - result.size.width).div(2), space.div(2).plus(4.dp.value)
                )
            )
        })
    }
}

绘制线性进度条

进度背景、进度

Screenshot_20240206_095955.png
@Composable
private fun LinearProgress(
    progress: Float = 0f,
    bgColor: Color = Color(0xFF1A5476),
    progressColor: Color = Color(0xFF1CCDCB),
    strokeWidth: Dp = 20.dp,
    cornerWidth: Dp = 40.dp
) {
    Box(
        Modifier.wrapContentSize()
//            .background(Color(0xFF1D405D))
    ) {
        Canvas(modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = cornerWidth)
            .height(strokeWidth), onDraw = {
            //背景
            drawRoundRect(
                bgColor, cornerRadius = CornerRadius(cornerWidth.value, cornerWidth.value)
            )
            drawRoundRect(
                progressColor, topLeft = Offset(0f, 0f), size = Size(
                    if (progress > 100f) size.width else progress.times(
                        size.width.div(100)
                    ), size.height
                ), cornerRadius = CornerRadius(cornerWidth.value, cornerWidth.value)
            )
        })
    }
}

绘制饼图

滑动手势方向拆分、旋转角度计算、扇形点击范围判断、放大缩小手势计算、文字填充(需要进一步实现)

dddd.png

Screen_recording_20240206_100025 00_00_00-00_00_30.gif
data class PieChartPiece(
    val color: Color, val startAngle: Float, val sweepAngle: Float
)

private fun containsPoint(//判断点是否在区域内
    width: Float, startAngle: Float, sweepAngle: Float, dotX: Int, dotY: Int
): Boolean {
    var region = Region();
    var path = Path()
    val rectF = RectF(0f, 0f, width, width)
    val rect = Rect(0, 0, width.toInt(), width.toInt())
    path.moveTo(width.div(2), width.div(2))
    path.lineTo(width, width.div(2))
    path.addArc(rectF, startAngle, sweepAngle)
    path.lineTo(width.div(2), width.div(2))
    path.close()
    region.setPath(path, Region(rect))
    return region.contains(dotX, dotY)
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PieChart(charts: MutableList<PieChartPiece>) {
    var rawX by remember {
        mutableFloatStateOf(0f)
    }
    var rawY by remember {
        mutableFloatStateOf(0f)
    }
    var degree by remember {
        mutableFloatStateOf(0f)
    }
    var initRawX = 0f;
    var initRawY = 0f;
    var pointerId by remember {
        mutableIntStateOf(1)
    }
    val textMeasurer = rememberTextMeasurer()
    Box(
        Modifier.wrapContentSize()
    ) {
        Canvas(modifier = Modifier
            .size(200.dp)
            .pointerInteropFilter(requestDisallowInterceptTouchEvent = RequestDisallowInterceptTouchEvent(),
                onTouchEvent = {
                    when (it.action) {
                        MotionEvent.ACTION_DOWN -> {
                            initRawX = it.x
                            initRawY = it.y
                            Log.e("TAG", "ACTION_DOWN $initRawX $initRawY ")
                            return@pointerInteropFilter true
                        }

                        MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
                            rawX = it.x - initRawX
                            rawY = it.y - initRawY
                            Log.e("TAG", "ACTION_MOVE_UP $rawX $rawY ")
                            if (rawX == 0f && rawY == 0f || MotionEvent.ACTION_UP == it.action) {
                                pointerId++
                            } else {
                                pointerId = 1
                            }
                            return@pointerInteropFilter true
                        }
                    }
                    false
                }), onDraw = {

            if (pointerId == 1 && rawX != 0f && rawY != 0f) {
                var arcLength = size.width.times(Math.PI).toFloat()
                var piece = arcLength.div(360)
                degree += if (abs(rawX) > abs(rawY)) {
                    if (size.height.div(2) > initRawY) {
                        rawX.div(piece)
                    } else {
                        -rawX.div(piece)
                    }
                } else {
                    if (size.width.div(2) > initRawX) {
                        -rawY.div(piece)
                    } else {
                        rawY.div(piece)
                    }

                }
            }
            Log.e("TAG", "$rawX $rawY degree  $degree")
            rotate(degree) {
                repeat(charts.size) {
                    val chart = charts[it]
                    var scaleCurrent =
                        if (pointerId != 1 && rawX == 0f && rawY == 0f && containsPoint(
                                size.width,
                                degree.plus(chart.startAngle),
                                chart.sweepAngle,
                                initRawX.toInt(),
                                initRawY.toInt()
                            )
                        ) 1.1f else 1f
                    Log.e("TAG", "scaleCurrent $scaleCurrent")
                    scale(
                        scaleCurrent,
                        pivot = Offset(
                            size.width.div(2), size.height.div(2)
                        )
                    ) {
                        drawArc(
                            chart.color,
                            chart.startAngle,
                            chart.sweepAngle,
                            useCenter = true,
                            topLeft = Offset.Zero,
                            style = Fill
                        )
                        val angle = chart.startAngle.plus(chart.sweepAngle.div(2))
                        var y = sin(angle) * size.width.div(2)
                        var x = cos(angle) * size.width.div(2)
                        when (if (angle > 360) angle.minus((angle % 360) * 360) else angle) {
                            in 0f..90f -> {//右下
                            }

                            in 91f..180f -> {//左下
                                x = -abs(x)
                            }

                            in 181f..270f -> {//左上
                                x = -abs(x)
                                y = -abs(y)
                            }

                            in 271f..360f -> {//右上
                                y = -abs(y)
                            }
                        }
                        var measurer = textMeasurer.measure(chart.sweepAngle.toString())
                        drawText(
                            measurer,
                            Color(0xFFab4b52),
                            topLeft = Offset(
                                size.width.div(2) - measurer.size.width.div(2) + x.div(2),
                                size.height.div(2) - measurer.size.height.div(2) + y.div(2)
                            )
                        )
                    }

                }
            }
        })


    }

}

//引用方式
val charts = mutableListOf<PieChartPiece>()
charts.add(PieChartPiece(Color.Yellow, 0f, 60f))
charts.add(PieChartPiece(Color.Cyan, 60f, 300f))
PieChart(charts)

绘制简单柱状图表

横竖坐标轴、0000代指字体尺寸测量

Screenshot_20240206_100247.png
@Composable
private fun LinearBar() {
    val withD = LocalConfiguration.current.screenWidthDp
    val m = rememberTextMeasurer()
    Box(
        Modifier
            .wrapContentSize()
            .background(Color.LightGray)
    ) {
        Canvas(modifier = Modifier
            .fillMaxWidth()
            .height(withD.div(2).dp), onDraw = {
            var wt = m.measure("0000").size.width
            var ht = m.measure("0000").size.height
            var hl = size.height - ht.toFloat()
            drawLine(//竖线
                Color.Magenta,
                start = Offset(wt.toFloat(), 0f),
                end = Offset(wt.toFloat(), hl)
            )
            drawLine(//横线
                Color.Magenta,
                start = Offset(wt.toFloat(), hl),
                end = Offset(size.width, hl)
            )
            for (i in 1 until 6) {
                drawText(
                    m,
                    "${i}000",
                    topLeft = Offset(0f, hl.times(5 - i).div(5)),
                    style = TextStyle(Color.DarkGray, fontStyle = FontStyle.Italic)
                )
            }
            val barWidth = wt.toFloat()
            val space = wt.div(2).toFloat()
            for (i in 1 until 6) {
                var topLeft = wt + space * i + barWidth * (i - 1) + barWidth.div(2)
                drawText(
                    m,
                    "item$i",
                    topLeft = Offset(
                        topLeft,
                        size.height - ht
                    ),
                    style = TextStyle(Color.DarkGray, fontWeight = FontWeight.Medium)
                )
                drawLine(
                    Color(0xFF1D405D),
                    start = Offset(topLeft + barWidth.div(2), Random.nextFloat() * hl),
                    end = Offset(topLeft + barWidth.div(2), hl),
                    strokeWidth = barWidth
                )
            }


        })
    }
}