Reputation: 115
I'm using a canvas in jetpack compose to draw a circle formed by multiple arches. And to for the arches to look better I set the cap to a round shape.
style = Stroke(width = chartBarWidth.toPx(),
cap = StrokeCap.Round)
The problem is that when using this stroke cap, is that the arch angle does not ajust to take into consideration the extra degrees produced by the StrokeCap. So I end up with arches overlapping .
How can I calculate the extra degrees produced by the strokeCap ?
Upvotes: 3
Views: 1779
Reputation: 67228
You need to calculate circumference of circle at center position and divide it to stroke width to get angle. Then add this to startAngle and remove 2 times from sweep angle to get same length after rounded stroke width is removed.
val width = size.width
val radius = width / 2f
val strokeWidth = 20.dp.toPx()
val circumference = 2 * Math.PI * (radius - strokeWidth / 2)
val strokeAngle = (strokeWidth / circumference * 180f).toFloat()
drawArc(
color = Color.Blue,
startAngle = 180f + strokeAngle,
sweepAngle = 180f - strokeAngle * 2,
useCenter = false,
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(width - strokeWidth, width - strokeWidth),
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
drawArc(
color = Color.Red,
startAngle = 0f + strokeAngle,
sweepAngle = 180f - strokeAngle * 2,
useCenter = false,
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(width - strokeWidth, width - strokeWidth),
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
You can add a coefficient if you wish to increase space between arcs
val strokeAngle = 1.1f * (strokeWidth / circumference * 180f).toFloat()
Here is a sample with with labels placed outside of arcs with rounded join
@Preview
@Composable
private fun PieChartWithLabels() {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val chartDataList = listOf(
ChartData(Pink400, 10f),
ChartData(Orange400, 20f),
ChartData(Yellow400, 15f),
ChartData(Green400, 5f),
ChartData(Blue400, 50f),
)
Canvas(
modifier = Modifier
.fillMaxWidth(.7f)
.aspectRatio(1f)
) {
val width = size.width
val radius = width / 2f
val strokeWidth = 20.dp.toPx()
val circumference = 2 * Math.PI * (radius - strokeWidth / 2)
val strokeAngle = 1.1f * (strokeWidth / circumference * 180f).toFloat()
var startAngle = -90f
for (index in 0..chartDataList.lastIndex) {
val chartData = chartDataList[index]
val sweepAngle = chartData.data.asAngle
val angleInRadians = (startAngle + sweepAngle / 2).degreeToAngle
drawArc(
color = chartData.color,
startAngle = startAngle + strokeAngle,
sweepAngle = sweepAngle - strokeAngle * 2,
useCenter = false,
topLeft = Offset(strokeWidth / 2, strokeWidth / 2),
size = Size(width - strokeWidth, width - strokeWidth),
style = Stroke(strokeWidth, cap = StrokeCap.Round)
)
startAngle += sweepAngle
val rectWidth = 20.dp.toPx()
drawRect(
color = Color.Red,
size = Size(rectWidth, rectWidth),
topLeft = Offset(
-rectWidth / 2 + center.x + (radius + strokeWidth) * cos(
angleInRadians
),
-rectWidth / 2 + center.y + (radius + strokeWidth) * sin(
angleInRadians
)
)
)
}
}
}
}
private val Float.degreeToAngle
get() = (this * Math.PI / 180f).toFloat()
@Immutable
data class ChartData(val color: Color, val data: Float)
Math behind getting circumference is we need to get center of stroke width to change this circle into lines with rounded caps and correctly measuring the stroke radius which is half of stroke witdh.
As you can see in the image below we are getting radius where red lines are drawn with strokeWidth/2 width.
@Preview
@Composable
fun CanvasTest() {
Canvas(
modifier = Modifier
.padding(50.dp)
.fillMaxWidth()
.aspectRatio(1f)
// .border(1.dp, Color.Green)
) {
val strokeWidth = 80f
drawLine(
color = Color.Blue,
start = Offset(strokeWidth / 2, strokeWidth / 2),
end = Offset(200f - strokeWidth / 2, strokeWidth / 2),
strokeWidth = strokeWidth,
cap = StrokeCap.Round
)
drawArc(
color = Color.Red,
useCenter = false,
topLeft = Offset.Zero,
size = Size(strokeWidth, strokeWidth),
startAngle = 90f,
sweepAngle = 180f,
style = Stroke(2.dp.toPx())
)
drawLine(
color = Color.Red,
start = Offset(0f, strokeWidth / 2 + 1),
end = Offset(strokeWidth / 2, strokeWidth / 2 + 1),
strokeWidth = 2f
)
drawLine(
color = Color.Red,
start = Offset(200f - strokeWidth / 2, strokeWidth / 2 + 1),
end = Offset(200f, strokeWidth / 2 + 1),
strokeWidth = 2f
)
}
}
If there were no round cap we were drawing blue line as a whole. With rounded caps we offset as strokeAngle at startAngle to not overlap from start position and remove - 2*strokeAngle to reserve space for arc only the where only blue line is drawn.
Upvotes: 12