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
相关产品推荐
相关产品推荐

