DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Jetpack Compose Canvas: Custom Drawing, Charts & Animations

Jetpack Compose Canvas: Custom Drawing, Charts & Animations

Jetpack Compose's Canvas API empowers you to create custom graphics, data visualizations, and fluid animations without fighting the Android graphics pipeline. Let's explore the powerful drawing primitives and real-world patterns.

Basic Drawing Primitives

drawCircle, drawRect, drawLine, drawArc

Canvas(modifier = Modifier.fillMaxSize()) {
    // Draw a circle at center
    drawCircle(
        color = Color.Blue,
        radius = 50.dp.toPx(),
        center = center
    )

    // Draw a rectangle
    drawRect(
        color = Color.Red,
        topLeft = Offset(100f, 100f),
        size = Size(200f, 150f)
    )

    // Draw a line
    drawLine(
        color = Color.Green,
        start = Offset(0f, 0f),
        end = Offset(size.width, size.height),
        strokeWidth = 4f
    )

    // Draw an arc
    drawArc(
        color = Color.Yellow,
        startAngle = 0f,
        sweepAngle = 270f,
        useCenter = true,
        topLeft = Offset(50f, 50f),
        size = Size(100f, 100f)
    )
}
Enter fullscreen mode Exit fullscreen mode

Advanced: Path & TextMeasurer

Path API enables complex shapes by combining lines, curves, and arcs:

val path = Path().apply {
    moveTo(size.width / 2, 0f)
    lineTo(size.width, size.height / 2)
    lineTo(size.width / 2, size.height)
    lineTo(0f, size.height / 2)
    close()
}
drawPath(path, color = Color.Magenta, style = Stroke(width = 2f))
Enter fullscreen mode Exit fullscreen mode

TextMeasurer measures text dimensions before drawing:

val textMeasurer = rememberTextMeasurer()
Canvas(modifier = Modifier.fillMaxSize()) {
    val textLayoutResult = textMeasurer.measure(
        text = AnnotatedString("Hello Compose"),
        style = TextStyle(fontSize = 20.sp)
    )
    drawText(textLayoutResult, topLeft = Offset(50f, 50f))
}
Enter fullscreen mode Exit fullscreen mode

Data Visualization: Bar Chart

@Composable
fun BarChart(data: List<Float>) {
    Canvas(modifier = Modifier.fillMaxWidth().height(300.dp)) {
        val barWidth = size.width / data.size
        val maxValue = data.maxOrNull() ?: 1f

        data.forEachIndexed { index, value ->
            val barHeight = (value / maxValue) * size.height
            drawRect(
                color = Color.Blue,
                topLeft = Offset(index * barWidth, size.height - barHeight),
                size = Size(barWidth * 0.8f, barHeight)
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Data Visualization: Line Chart

@Composable
fun LineChart(points: List<Float>) {
    Canvas(modifier = Modifier.fillMaxWidth().height(300.dp)) {
        val pointCount = points.size
        val xStep = size.width / (pointCount - 1)
        val maxValue = points.maxOrNull() ?: 1f

        val path = Path()
        points.forEachIndexed { index, value ->
            val x = index * xStep
            val y = size.height - (value / maxValue) * size.height
            if (index == 0) path.moveTo(x, y) else path.lineTo(x, y)
        }

        drawPath(path, Color.Blue, style = Stroke(width = 3f))
    }
}
Enter fullscreen mode Exit fullscreen mode

Data Visualization: Pie Chart

@Composable
fun PieChart(data: Map<String, Float>) {
    Canvas(modifier = Modifier.size(300.dp)) {
        val total = data.values.sum()
        val center = Offset(size.width / 2, size.height / 2)
        val radius = size.width / 3

        var startAngle = 0f
        data.forEach { (_, value) ->
            val sweepAngle = (value / total) * 360f
            drawArc(
                color = Color(kotlin.random.Random.nextInt(0xFF000000.toInt(), 0xFFFFFFFF.toInt())),
                startAngle = startAngle,
                sweepAngle = sweepAngle,
                useCenter = true,
                size = Size(radius * 2, radius * 2),
                topLeft = center - Offset(radius, radius)
            )
            startAngle += sweepAngle
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Animated Progress Ring

Combine Canvas with Compose's animation system:

@Composable
fun AnimatedProgressRing(progress: Float) {
    val animatedProgress = animateFloatAsState(progress)

    Canvas(modifier = Modifier.size(200.dp)) {
        drawArc(
            color = Color.LightGray,
            startAngle = 0f,
            sweepAngle = 360f,
            useCenter = false,
            style = Stroke(width = 8f)
        )

        drawArc(
            color = Color.Blue,
            startAngle = -90f,
            sweepAngle = animatedProgress.value * 360f,
            useCenter = false,
            style = Stroke(width = 8f)
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Performance Tips

  • Measure expensive operations: Use rememberTextMeasurer() to avoid repeated measurements
  • Batch drawing calls: Group related drawX() calls together
  • Use remember for paths: Recalculate paths only when data changes
  • Optimize for rotation: For rotated elements, use translate() and rotate() to transform the canvas

The Canvas API is your gateway to rich, interactive visualizations and custom UI experiences in Compose.


8 Android App Templates → https://myougatheax.gumroad.com

Top comments (0)