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 theCanvas
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 usingOffset
.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 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.