avram andrei tiberiu
avram andrei tiberiu

Reputation: 115

Jetpack compose Canvas Arch extra Stroke

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 ?enter image description here

Upvotes: 3

Views: 1779

Answers (1)

Thracian
Thracian

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.

enter image description here

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)

Edit

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.

enter image description here

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

Related Questions