Jetpack Compose 11: Canvas and Custom Drawing
Teqani Blogs
Writer at Teqani
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)
All blogs are certified by our company and reviewed by our specialists
Issue Number: #65b340a1-4696-4ee6-8fa4-b42b1778be7a