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:
Canvasis the composable that provides the drawing surface.Modifier.fillMaxSize()ensures theCanvastakes up the entire screen.
- Drawing Operations:
drawCircledraws a circle on the canvas.colorsets the color of the circle (Color.Blue).centerspecifies the center point of the circle usingOffset.radiussets the radius of the circle.styledefines the drawing style. Here,Strokeis 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.moveTosets the starting point of the path.lineToadds a line segment to the path.closecloses the path by connecting the last point to the starting point.
- Drawing the Path:
drawPathdraws the defined path on the canvas.pathspecifies the path to be drawn.colorsets the color of the path.styledefines the drawing style. Here,Strokeis 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
TextMeasureris required to measure the text for drawing on a Canvas in Compose. - The
textMeasurer.measureis invoked to get aTextLayoutResult. - 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.