You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中Canvas绘制圆角饼图的实现问题

Jetpack Compose环形饼图实现圆角饼块的解决方案

你遇到的问题核心是:StrokeCap.Round仅对单条路径的首尾端点生效,而环形饼图的每个弧段需要内外两侧都有圆角,且多个弧段独立绘制时,单纯的StrokeCap无法实现完整的圆角效果。

下面提供两种可行的实现方案:

方案一:drawArc配合drawCircle快速实现

通过在每个弧段的起点和终点,分别在外圈和内圈绘制小圆形,模拟圆角效果,代码简洁易理解:

@Composable
fun Chart() {
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1f)
    ) {
        val width = size.width
        val outerRadius = width / 2f
        val strokeWidth = outerRadius * .3f
        val innerRadius = outerRadius - strokeWidth
        var startAngle = 0f

        val items = listOf(25f, 25f, 25f, 25f, 25f, 25f, 25f, 25f)
        val gapAngle = 5f // 饼块间的间隙角度

        items.forEach { percentage ->
            val sweepAngle = percentage.toAngle - gapAngle
            val color = Color.Gray // 可替换为不同颜色区分饼块

            // 绘制核心弧段
            drawArc(
                color = color,
                startAngle = startAngle + gapAngle/2, // 让弧段居中在分配的角度区间
                sweepAngle = sweepAngle,
                useCenter = false,
                topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
                size = Size(width - strokeWidth, width - strokeWidth),
                style = Stroke(strokeWidth)
            )

            // 转换起点角度为弧度(Compose Canvas角度顺时针,0点在右侧)
            val startRadians = (startAngle + gapAngle/2).toRadians
            // 绘制起点的外圈圆角
            drawCircle(
                color = color,
                radius = strokeWidth / 2,
                center = Offset(
                    outerRadius + outerRadius * cos(startRadians),
                    outerRadius + outerRadius * sin(startRadians)
                )
            )
            // 绘制起点的内圈圆角
            drawCircle(
                color = color,
                radius = strokeWidth / 2,
                center = Offset(
                    outerRadius + innerRadius * cos(startRadians),
                    outerRadius + innerRadius * sin(startRadians)
                )
            )

            // 计算终点角度并转换为弧度
            val endAngle = startAngle + gapAngle/2 + sweepAngle
            val endRadians = endAngle.toRadians
            // 绘制终点的外圈圆角
            drawCircle(
                color = color,
                radius = strokeWidth / 2,
                center = Offset(
                    outerRadius + outerRadius * cos(endRadians),
                    outerRadius + outerRadius * sin(endRadians)
                )
            )
            // 绘制终点的内圈圆角
            drawCircle(
                color = color,
                radius = strokeWidth / 2,
                center = Offset(
                    outerRadius + innerRadius * cos(endRadians),
                    outerRadius + innerRadius * sin(endRadians)
                )
            )

            startAngle += percentage.toAngle
        }
    }
}

private val Float.toAngle: Float
    get() = this * 360 / 100 // 修正为360,对应整个圆周的百分比转换

private val Float.toRadians: Float
    get() = Math.toRadians(this.toDouble()).toFloat()

方案二:使用Path构建完整饼块路径(更精确)

通过Path构建包含外弧、圆角、内弧的完整轮廓,填充后得到更自然的圆角效果,适合需要高精度的场景:

@Composable
fun Chart() {
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .aspectRatio(1f)
    ) {
        val width = size.width
        val outerRadius = width / 2f
        val strokeWidth = outerRadius * .3f
        val innerRadius = outerRadius - strokeWidth
        val cornerRadius = strokeWidth / 2f // 圆角半径等于stroke宽度的一半
        var startAngle = 0f

        val items = listOf(25f, 25f, 25f, 25f, 25f, 25f, 25f, 25f)
        val gapAngle = 5f

        items.forEach { percentage ->
            val sweepAngle = percentage.toAngle - gapAngle
            val color = Color.Gray

            val path = Path()
            val startAngleAdjusted = startAngle + gapAngle/2
            val endAngle = startAngleAdjusted + sweepAngle

            // 计算起点的内外圈坐标
            val startOuterX = outerRadius + outerRadius * cos(startAngleAdjusted.toRadians)
            val startOuterY = outerRadius + outerRadius * sin(startAngleAdjusted.toRadians)
            val startInnerX = outerRadius + innerRadius * cos(startAngleAdjusted.toRadians)
            val startInnerY = outerRadius + innerRadius * sin(startAngleAdjusted.toRadians)

            // 计算终点的内外圈坐标
            val endOuterX = outerRadius + outerRadius * cos(endAngle.toRadians)
            val endOuterY = outerRadius + outerRadius * sin(endAngle.toRadians)
            val endInnerX = outerRadius + innerRadius * cos(endAngle.toRadians)
            val endInnerY = outerRadius + innerRadius * sin(endAngle.toRadians)

            // 构建完整饼块路径
            path.apply {
                // 移动到起点外圆的圆角起始点
                moveTo(
                    startOuterX + cornerRadius * cos(startAngleAdjusted.toRadians),
                    startOuterY + cornerRadius * sin(startAngleAdjusted.toRadians)
                )
                // 绘制外圈弧
                arcTo(
                    rect = Rect(
                        left = cornerRadius,
                        top = cornerRadius,
                        right = width - cornerRadius,
                        bottom = width - cornerRadius
                    ),
                    startAngleDegrees = startAngleAdjusted,
                    sweepAngleDegrees = sweepAngle,
                    forceMoveTo = false
                )
                // 绘制终点外圆的圆角(半圆)
                arcTo(
                    rect = Rect(
                        left = endOuterX - cornerRadius,
                        top = endOuterY - cornerRadius,
                        right = endOuterX + cornerRadius,
                        bottom = endOuterY + cornerRadius
                    ),
                    startAngleDegrees = endAngle - 90,
                    sweepAngleDegrees = 180,
                    forceMoveTo = false
                )
                // 反向绘制内圈弧
                arcTo(
                    rect = Rect(
                        left = strokeWidth + cornerRadius,
                        top = strokeWidth + cornerRadius,
                        right = width - strokeWidth - cornerRadius,
                        bottom = width - strokeWidth - cornerRadius
                    ),
                    startAngleDegrees = endAngle,
                    sweepAngleDegrees = -sweepAngle,
                    forceMoveTo = false
                )
                // 绘制起点内圆的圆角(半圆)
                arcTo(
                    rect = Rect(
                        left = startInnerX - cornerRadius,
                        top = startInnerY - cornerRadius,
                        right = startInnerX + cornerRadius,
                        bottom = startInnerY + cornerRadius
                    ),
                    startAngleDegrees = startAngleAdjusted + 90,
                    sweepAngleDegrees = 180,
                    forceMoveTo = false
                )
                close() // 闭合路径
            }

            // 填充路径完成饼块绘制
            drawPath(path = path, color = color)

            startAngle += percentage.toAngle
        }
    }
}

private val Float.toAngle: Float
    get() = this * 360 / 100

private val Float.toRadians: Float
    get() = Math.toRadians(this.toDouble()).toFloat()

说明

  • 原代码中toAngle用180/100是逻辑错误,修正为360/100才能正确将百分比转换为圆周角度。
  • StrokeCap.Round无法实现环形饼图的双圆角效果,因为它仅处理单条路径的首尾端点,且无法覆盖内外两侧。

内容的提问来源于stack exchange,提问作者Abp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 20:50:59