Jetpack Compose 11: Canvas and Custom Drawing

Jetpack Compose 11: Canvas and Custom Drawing

TB

Teqani Blogs

Writer at Teqani

June 5, 20253 min read

Jetpack Compose offers powerful low-level drawing APIs when you need fine-grained control over your UI, from custom shapes to hand-drawn charts. This article explores how to use Canvas and Custom Drawing in Jetpack Compose for creating unique and performant user interfaces. Understanding these techniques is crucial for any serious Android developer.



1. Canvas – For Freehand Drawing

The simplest way to draw directly on the screen is using Canvas. It's great for shapes, paths, charts, and freehand UIs. The Canvas API provides a flexible way to render graphics.



@Composable
fun CanvasExample() {
    Canvas(modifier = Modifier.size(200.dp)) {
        drawCircle(
            color = Color.Blue,
            radius = size.minDimension / 3,
            center = center
        )
    }
}


2. Modifier.drawWithCache – Optimized Drawing

Use this when drawing doesn’t change often. It caches the drawing to improve performance. Ideal for static or semi-static custom UI. This optimization technique is essential for smooth UI rendering.



@Composable
fun DrawWithCacheExample() {
    Box(
        modifier = Modifier
            .size(150.dp)
            .drawWithCache {
                val gradient = Brush.radialGradient(
                    colors = listOf(Color.Yellow, Color.Red),
                    center = center,
                    radius = size.minDimension / 2
                )
                onDrawBehind {
                    drawRect(gradient)
                }
            }
    )
}


3. Custom Painter – Create Your Paint Logic

Define a custom painter by extending Painter. Use for advanced reusable graphics or exportable elements. This approach promotes code reusability and modularity.



class MyPainter : Painter() {
    override val intrinsicSize: Size
        get() = Size.Unspecified
    override fun DrawScope.onDraw() {
        drawLine(Color.Black, Offset.Zero, Offset(size.width, size.height), strokeWidth = 4f)
    }
}

@Composable
fun CustomPainterExample() {
    Canvas(modifier = Modifier.size(200.dp)) {
        with(MyPainter()) {
            draw(this@Canvas)
        }
    }
}


4. DrawScope – Provides Drawing Power Inside Canvas

All Canvas and Painter use DrawScope under the hood, which gives access to size, drawCircle(), drawRect(), drawPath(), etc., and drawContext.canvas.nativeCanvas – low-level access. Understanding DrawScope unlocks advanced drawing capabilities.



@Composable
fun PathDrawingExample() {
    Canvas(modifier = Modifier.size(200.dp)) {
        val path = Path().apply {
            moveTo(0f, size.height / 2)
            quadraticBezierTo(
                size.width / 2, 0f,
                size.width, size.height / 2
            )
        }
        drawPath(path, color = Color.Magenta, style = Stroke(width = 5f))
    }
}


5. Modifier.graphicsLayer – 2D Transformations

Rotate, scale, and translate components on canvas or view level. Combine with animation to create dynamic effects. GraphicsLayer provides powerful transformation options.



@Composable
fun GraphicsLayerDrawingExample() {
    Box(
        modifier = Modifier
            .size(100.dp)
            .graphicsLayer(rotationZ = 45f, scaleX = 1.2f)
            .background(Color.Green)
    )
}


Conclusion

  • Canvas: Freeform shapes and graphics
  • drawWithCache: Cached drawing (e.g., gradients, patterns)
  • Custom Painter: Reusable drawing logic
  • DrawScope: Advanced shapes and path drawing
  • graphicsLayer: Transformations (rotate, scale, skew)
TB

Teqani Blogs

Verified
Writer at Teqani

Senior Software Engineer with 10 years of experience

June 5, 2025
Teqani Certified

All blogs are certified by our company and reviewed by our specialists
Issue Number: #65b340a1-4696-4ee6-8fa4-b42b1778be7a