我正在尝试绘制带有部分的饼图并显示每个部分的标签。标签应显示在每个部分的开头,并距饼图 10dp 的内边距。
这是我的代码:
@Composable
fun PieChartLocal(
modifier: Modifier = Modifier,
width: Float,
thickness: Float,
duration: Int,
sections: List<Section>,
) {
val sweepAngles = remember(sections) { findSweepAngles(sections) }
val animateFloat = remember { Animatable(0f) }
val density = LocalDensity.current
val paddingDp = 18.dp
val textSizeDp = 10.sp
val paddingPx = with(density) { paddingDp.toPx() }
val textSizePx = with(density) { textSizeDp.toPx() }
LaunchedEffect(animateFloat) {
animateFloat.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = duration, easing = LinearEasing)
)
}
Canvas(
modifier = modifier
.size((width + 2 * (paddingDp.value + textSizeDp.value)).dp)
) {
var startAngle = 0f
val radius =
(size.minDimension - 2 * (paddingPx + textSizePx)) / 2
val center = size.center
for (i in sweepAngles.indices) {
val sweepAngle = sweepAngles[i] * animateFloat.value
val color = sections[i].color
drawArc(
color = color,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(width = thickness),
topLeft = Offset(center.x - radius, center.y - radius),
size = Size(radius * 2, radius * 2),
)
val angleInRadians = Math.toRadians(startAngle.toDouble())
val textRadius = radius + paddingPx
val x = center.x + textRadius * cos(angleInRadians).toFloat()
val y = center.y + textRadius * sin(angleInRadians).toFloat()
val percentage = (sweepAngles[i] / ROUND_ANGLE * 100).toInt()
drawContext.canvas.nativeCanvas.apply {
drawText(
"$percentage%",
x,
y,
TextPaint().apply {
textAlign = Paint.Align.CENTER
textSize = textSizePx
}
)
}
startAngle += sweepAngle
}
}
}
private fun findSweepAngles(sections: List<Section>): List<Float> {
val values = sections.map(Section::value)
val sumValues = values.sum()
return values.map { value -> ROUND_ANGLE * value / sumValues }
}
问题是,对于某些标签,我与饼图的偏移量不正确。对于许多标签,它等于预期的 10dp,而对于其余标签,它完全不存在。可能是什么问题?
请帮助我..
您可以参考这个答案绘制一个饼图,每个部分的中心都有文本。
当您希望在段开头绘制文本文本时,您可以使用等式,例如
drawText(
textLayoutResult = textMeasureResult,
color = Color.DarkGray,
topLeft = Offset(
x = center.x + textOffsetX + (offset + outerRadius) * cos,
y = center.y + textOffsetY + (offset + outerRadius) * sin
)
)
textOffset 是我们用于基于文本矩形从文本边界左上角偏移的东西。
offset 在我们的例子中是额外的用户偏移量 10.dp 外半径是图表,在下面的gif中带有蓝色圆圈。
在 gif 中,原因文本位于段下方,我将弧线和段绘制在一起。如果它们相交,请在另一个循环中绘制文本
@Preview
@Composable
private fun PieChartWithText() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
var chartStartAngle by remember {
mutableFloatStateOf(0f)
}
Text("Chart Start angle: ${chartStartAngle.toInt()}")
Slider(
value = chartStartAngle,
onValueChange = {
chartStartAngle = it
},
valueRange = 0f..360f
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
contentAlignment = Alignment.Center
) {
val chartDataList = listOf(
ChartData(Pink400, 20f),
ChartData(Orange400, 30f),
ChartData(Yellow400, 40f),
ChartData(Blue400, 10f)
)
val textMeasurer = rememberTextMeasurer()
val textMeasureResults = remember(chartDataList) {
chartDataList.map {
textMeasurer.measure(
text = "%${it.data.toInt()}",
style = TextStyle(
fontSize = 18.sp
)
)
}
}
Canvas(
modifier = Modifier
.padding(24.dp)
.fillMaxWidth()
.aspectRatio(1f)
) {
val width = size.width
val radius = width * .25f
val strokeWidth = radius * .7f
val outerRadius = radius + strokeWidth + strokeWidth / 2
val diameter = (radius + strokeWidth) * 2
val topLeft = (width - diameter) / 2f
var startAngle = chartStartAngle
for (index in 0..chartDataList.lastIndex) {
val chartData = chartDataList[index]
val sweepAngle = chartData.data.asAngle
val textMeasureResult = textMeasureResults[index]
val textSize = textMeasureResult.size
val offset = 10.dp.toPx()
println("startAngle: $startAngle, sweepAngle: $sweepAngle")
drawArc(
color = chartData.color,
startAngle = startAngle,
sweepAngle = sweepAngle,
useCenter = false,
topLeft = Offset(topLeft, topLeft),
size = Size(diameter, diameter),
style = Stroke(strokeWidth)
)
val rect = textMeasureResult.getBoundingBox(0)
val cos = cos(startAngle.degreeToRadian)
val sin = sin(startAngle.degreeToRadian)
var textOffsetX: Int = 0
var textOffsetY: Int = 0
// when (startAngle) {
// in 0f..90f -> {
// textOffsetX = 0
// textOffsetY = 0
// }
//
// in 90f..180f -> {
// textOffsetX = -textSize.width
// textOffsetY = 0
// }
//
// in 180f..270f -> {
// textOffsetX = -textSize.width
// textOffsetY = -textSize.height
// }
//
// else -> {
// textOffsetX = 0
// textOffsetY = -textSize.height
// }
// }
drawCircle(
color = Color.Blue,
radius = outerRadius,
style = Stroke(2.dp.toPx())
)
// drawCircle(
// color = Color.Magenta,
// radius = outerRadius + offset,
// style = Stroke(2.dp.toPx())
// )
drawRect(
color = Color.Black,
topLeft = Offset(
x = rect.topLeft.x + center.x + textOffsetX + (offset + outerRadius) * cos,
y = rect.topLeft.y + center.y + textOffsetY + (offset + outerRadius) * sin
),
size = textSize.toSize(),
style = Stroke(3.dp.toPx())
)
drawText(
textLayoutResult = textMeasureResult,
color = Color.DarkGray,
topLeft = Offset(
x = center.x + textOffsetX + (offset + outerRadius) * cos,
y = center.y + textOffsetY + (offset + outerRadius) * sin
)
)
startAngle += sweepAngle
startAngle = startAngle % 360
}
}
}
}
}
private val Float.degreeToRadian
get() = (this * Math.PI / 180f).toFloat()
private val Float.asAngle: Float
get() = this * 360f / 100f
@Immutable
data class ChartData(val color: Color, val data: Float)
文本矩形的左上角指向每个段的开头。下一步是更改每个象限的矩形中线段的起始位置应接触的位置。
Quadrant 1 -> rect bottom start
Quadrant 2 -> rect bottom end
Quadrant 3 -> rect top end
Quadrant 4 -> rect top start
添加了
when (startAngle) {
in 0f..90f -> {
textOffsetX = 0
textOffsetY = 0
}
in 90f..180f -> {
textOffsetX = -textSize.width
textOffsetY = 0
}
in 180f..270f -> {
textOffsetX = -textSize.width
textOffsetY = -textSize.height
}
else -> {
textOffsetX = 0
textOffsetY = -textSize.height
}
}
取消注释注释片段时的结果