Canvas Composable Basics in Jetpack Compose: A Comprehensive Guide

Jetpack Compose, Android’s modern UI toolkit, provides a flexible and declarative way to build user interfaces. One of its powerful features is the Canvas composable, which allows you to draw custom graphics, animations, and visualizations directly within your Compose UI. Mastering the basics of the Canvas composable is essential for creating sophisticated and unique user experiences. In this comprehensive guide, we will explore the Canvas composable, cover fundamental concepts, and provide practical examples to help you get started.

What is the Canvas Composable?

The Canvas composable in Jetpack Compose is a drawing board where you can programmatically create and render shapes, images, text, and more. It gives you low-level control over the visual elements in your app, enabling highly customized graphics that go beyond standard UI components.

Why Use the Canvas Composable?

  • Custom Graphics: Create unique designs, illustrations, and visual elements.
  • Data Visualization: Draw charts, graphs, and data-driven visuals.
  • Animations: Implement custom animations by redrawing elements on the Canvas.
  • Game Development: Develop 2D game interfaces and graphics.

Setting Up Your Project

Before diving into the code, ensure you have a basic Jetpack Compose project set up. Include the necessary Compose dependencies in your build.gradle file:


dependencies {
    implementation("androidx.compose.ui:ui:1.6.0")
    implementation("androidx.compose.material:material:1.6.0")
    implementation("androidx.compose.ui:ui-tooling-preview:1.6.0")
    implementation("androidx.activity:activity-compose:1.8.2")
}

Ensure you are using the latest stable versions of the Compose libraries.

Basic Canvas Implementation

Let’s start with a simple example of drawing a circle on the Canvas.


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun SimpleCanvasExample() {
    Canvas(
        modifier = Modifier.fillMaxSize()
    ) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawCircle(
            color = Color.Blue,
            center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
            radius = canvasWidth / 4,
            style = Stroke(width = 5f)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewSimpleCanvasExample() {
    SimpleCanvasExample()
}

Explanation:

  • Canvas Composable:
    • Canvas is the composable that provides the drawing surface.
    • Modifier.fillMaxSize() ensures the Canvas takes up the entire screen.
  • Drawing Operations:
    • drawCircle draws a circle on the canvas.
    • color sets the color of the circle (Color.Blue).
    • center specifies the center point of the circle using Offset.
    • radius sets the radius of the circle.
    • style defines the drawing style. Here, Stroke is used to create an outline with a specified width.

Drawing Shapes

The Canvas composable provides various methods for drawing common shapes.

Drawing a Rectangle


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun DrawRectangleExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawRect(
            color = Color.Green,
            topLeft = Offset(x = canvasWidth / 4, y = canvasHeight / 4),
            size = Size(width = canvasWidth / 2, height = canvasHeight / 2),
            style = Fill
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDrawRectangleExample() {
    DrawRectangleExample()
}

Here, drawRect is used to draw a rectangle. topLeft specifies the starting point, and size defines the width and height. The Fill style fills the rectangle with the specified color.

Drawing a Line


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun DrawLineExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawLine(
            color = Color.Red,
            start = Offset(x = canvasWidth / 4, y = canvasHeight / 4),
            end = Offset(x = 3 * canvasWidth / 4, y = 3 * canvasHeight / 4),
            strokeWidth = 5.dp.toPx(),
            cap = StrokeCap.Round
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDrawLineExample() {
    DrawLineExample()
}

The drawLine function draws a line between two points, start and end. strokeWidth sets the thickness of the line, and cap specifies the shape of the line ends.

Drawing an Arc


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun DrawArcExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawArc(
            color = Color.Magenta,
            startAngle = 0f,
            sweepAngle = 270f,
            useCenter = false,
            topLeft = androidx.compose.ui.geometry.Offset(x = canvasWidth / 4, y = canvasHeight / 4),
            size = Size(width = canvasWidth / 2, height = canvasHeight / 2),
            style = Stroke(width = 5.dp.toPx())
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDrawArcExample() {
    DrawArcExample()
}

The drawArc function draws an arc (a portion of an ellipse). startAngle specifies the angle at which the arc starts, and sweepAngle defines how far the arc extends. useCenter determines whether the arc is connected to the center, and topLeft and size define the bounding box of the arc.

Customizing Drawings with Styles

You can customize the appearance of your drawings by using different styles.

Fill and Stroke


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun FillAndStrokeExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawCircle(
            color = Color.Blue,
            center = Offset(x = canvasWidth / 4, y = canvasHeight / 2),
            radius = canvasWidth / 8,
            style = Fill
        )

        drawCircle(
            color = Color.Red,
            center = Offset(x = 3 * canvasWidth / 4, y = canvasHeight / 2),
            radius = canvasWidth / 8,
            style = Stroke(width = 5.dp.toPx())
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewFillAndStrokeExample() {
    FillAndStrokeExample()
}

This example draws two circles. The first is filled with blue color using the Fill style, and the second is outlined with a red stroke using the Stroke style.

Colors and Transparency


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun ColorsAndTransparencyExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        drawRect(
            color = Color.Yellow.copy(alpha = 0.5f),
            topLeft = Offset(x = canvasWidth / 4, y = canvasHeight / 4),
            size = androidx.compose.ui.geometry.Size(width = canvasWidth / 2, height = canvasHeight / 2),
            style = Fill
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewColorsAndTransparencyExample() {
    ColorsAndTransparencyExample()
}

This code draws a semi-transparent yellow rectangle. The copy function is used to set the alpha (transparency) value of the color.

Drawing Paths

For more complex drawings, you can use the Path class to define custom shapes.


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun DrawPathExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        val path = Path().apply {
            moveTo(canvasWidth / 4, canvasHeight / 2)
            lineTo(canvasWidth / 2, canvasHeight / 4)
            lineTo(3 * canvasWidth / 4, canvasHeight / 2)
            lineTo(canvasWidth / 2, 3 * canvasHeight / 4)
            close()
        }

        drawPath(
            path = path,
            color = Color.Cyan,
            style = Stroke(width = 5.dp.toPx())
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDrawPathExample() {
    DrawPathExample()
}

Explanation:

  • Path Creation:
    • Path() initializes a new path.
    • moveTo sets the starting point of the path.
    • lineTo adds a line segment to the path.
    • close closes the path by connecting the last point to the starting point.
  • Drawing the Path:
    • drawPath draws the defined path on the canvas.
    • path specifies the path to be drawn.
    • color sets the color of the path.
    • style defines the drawing style. Here, Stroke is used to outline the path with a specified width.

Transformations

The Canvas also supports transformations such as translation, rotation, and scaling.

Translation


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun TranslateExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        translate(left = canvasWidth / 4, top = canvasHeight / 4) {
            drawCircle(
                color = Color.Yellow,
                center = Offset(x = 0f, y = 0f),
                radius = canvasWidth / 8
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewTranslateExample() {
    TranslateExample()
}

The translate function shifts the coordinate system by the specified amount. The circle is drawn at the new origin.

Rotation


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun RotateExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        rotate(degrees = 45f) {
            drawRect(
                color = Color.Gray,
                topLeft = Offset(x = canvasWidth / 4, y = canvasHeight / 4),
                size = androidx.compose.ui.geometry.Size(width = canvasWidth / 2, height = canvasHeight / 2)
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewRotateExample() {
    RotateExample()
}

The rotate function rotates the coordinate system by the specified angle. The rectangle is drawn rotated around the origin.

Scale


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun ScaleExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        scale(scaleX = 0.5f, scaleY = 0.5f) {
            drawCircle(
                color = Color.Black,
                center = Offset(x = canvasWidth / 2, y = canvasHeight / 2),
                radius = canvasWidth / 4
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewScaleExample() {
    ScaleExample()
}

The scale function scales the coordinate system by the specified factors. The circle is drawn scaled down by a factor of 0.5 in both the X and Y directions.

Clipping

You can clip drawings to a specific region using the clipRect or clipPath functions.


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun ClipRectExample() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        clipRect(
            left = canvasWidth / 4,
            top = canvasHeight / 4,
            right = 3 * canvasWidth / 4,
            bottom = 3 * canvasHeight / 4
        ) {
            drawRect(
                color = Color.DarkGray,
                topLeft = Offset(x = 0f, y = 0f),
                size = androidx.compose.ui.geometry.Size(width = canvasWidth, height = canvasHeight)
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewClipRectExample() {
    ClipRectExample()
}

The clipRect function clips the drawing area to a rectangular region. Only the portion of the rectangle within the clipping region is drawn.

Drawing Text

You can also draw text on the Canvas using the drawText function.


import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawText
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp

@Composable
fun DrawTextExample() {
    val textMeasurer = rememberTextMeasurer()

    Canvas(modifier = Modifier.fillMaxSize()) {
        val canvasWidth = size.width
        val canvasHeight = size.height

        val textLayoutResult = textMeasurer.measure(
            text = "Hello, Canvas!",
            style = TextStyle(fontSize = 24.sp)
        )

        drawText(
            textLayoutResult = textLayoutResult,
            color = Color.Black,
            topLeft = Offset(x = canvasWidth / 2 - textLayoutResult.size.width / 2, y = canvasHeight / 2 - textLayoutResult.size.height / 2)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewDrawTextExample() {
    DrawTextExample()
}

Explanation:

  • A TextMeasurer is required to measure the text for drawing on a Canvas in Compose.
  • The textMeasurer.measure is invoked to get a TextLayoutResult.
  • Then use `drawText` with the `TextLayoutResult` to render the text onto the Canvas.

Conclusion

The Canvas composable in Jetpack Compose is a versatile tool for creating custom graphics and visualizations in your Android applications. By mastering the basics of drawing shapes, customizing styles, applying transformations, and clipping regions, you can create highly engaging and unique user experiences. Use these fundamental concepts and examples to unleash your creativity and build impressive visuals in your Compose UI.