Jetpack Compose, Android’s modern UI toolkit, provides powerful and flexible tools for creating stunning user interfaces. One of the key features is the ability to perform custom drawing directly within your composables. By leveraging custom drawing techniques, you can create unique and visually appealing components that go beyond the standard UI elements. This article will guide you through various custom drawing techniques in Jetpack Compose, complete with detailed explanations and code samples.
Why Custom Drawing in Jetpack Compose?
Custom drawing allows you to:
- Create unique UI elements not available in the standard library.
- Implement advanced animations and visual effects.
- Fine-tune the appearance of your UI components.
Getting Started with Custom Drawing
The primary API for custom drawing in Jetpack Compose is the Canvas
composable within the DrawModifier
. You interact with the Canvas
inside the drawBehind
, drawWithContent
, or drawWithCache
modifiers. Each provides different benefits, which we will explore.
Step 1: Setting up a Basic Canvas
First, let’s set up a basic canvas to draw on:
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun BasicCanvasExample() {
Canvas(
modifier = Modifier.size(200.dp),
onDraw = {
// Drawing operations will go here
drawCircle(
color = Color.Blue,
center = center,
radius = 50.dp.toPx()
)
}
)
}
@Preview(showBackground = true)
@Composable
fun PreviewBasicCanvasExample() {
BasicCanvasExample()
}
Explanation:
- We use the
Canvas
composable and set its size to 200dp. - The
onDraw
lambda provides aDrawScope
where you can perform drawing operations. drawCircle
draws a blue circle in the center of the canvas with a radius of 50dp.
Step 2: Drawing Shapes
Compose provides functions to draw various shapes:
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun DrawingShapesExample() {
Canvas(
modifier = Modifier.size(200.dp),
onDraw = {
drawCircle(
color = Color.Blue,
center = center,
radius = 50.dp.toPx()
)
drawRect(
color = Color.Red,
topLeft = Offset(50.dp.toPx(), 50.dp.toPx()),
size = Size(100.dp.toPx(), 50.dp.toPx())
)
drawLine(
color = Color.Green,
start = Offset(0.dp.toPx(), 0.dp.toPx()),
end = Offset(size.width, size.height),
strokeWidth = 5.dp.toPx()
)
}
)
}
@Preview(showBackground = true)
@Composable
fun PreviewDrawingShapesExample() {
DrawingShapesExample()
}
Explanation:
drawCircle
draws a circle.drawRect
draws a rectangle with a specified top-left corner and size.drawLine
draws a line between two points with a specified stroke width.
Step 3: Using DrawModifiers: drawBehind, drawWithContent, and drawWithCache
Jetpack Compose offers several modifiers to customize the drawing process:
drawBehind
: Draw content behind the composable.drawWithContent
: Draw custom content around the existing content.drawWithCache
: Cache drawing instructions to improve performance for complex and static drawings.
Example: drawBehind
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun DrawBehindExample() {
Box(
modifier = Modifier
.size(200.dp)
.drawBehind {
drawCircle(
color = Color.Cyan,
radius = 70.dp.toPx(),
center = center
)
}
.background(Color.LightGray)
)
}
@Preview(showBackground = true)
@Composable
fun PreviewDrawBehindExample() {
DrawBehindExample()
}
Explanation:
drawBehind
draws a cyan circle behind theBox
composable, which is filled with a light gray background.
Example: drawWithContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun DrawWithContentExample() {
Box(
modifier = Modifier
.size(200.dp)
.drawWithContent {
drawContent() // Draw the original content first
drawRect(
color = Color.Black,
topLeft = Offset(0.dp.toPx(), 0.dp.toPx()),
size = size,
alpha = 0.2f // Semi-transparent
)
}
.background(Color.White)
)
}
@Preview(showBackground = true)
@Composable
fun PreviewDrawWithContentExample() {
DrawWithContentExample()
}
Explanation:
drawWithContent
first draws the original content usingdrawContent()
and then draws a semi-transparent black rectangle over it.
Example: drawWithCache
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun DrawWithCacheExample() {
Box(
modifier = Modifier
.size(200.dp)
.drawWithCache {
val circleRadius = size.minDimension / 4
val circleColor = Color.Red
onDrawWithContent {
drawContent()
drawCircle(
color = circleColor,
radius = circleRadius,
center = center
)
}
}
.background(Color.Yellow)
)
}
@Preview(showBackground = true)
@Composable
fun PreviewDrawWithCacheExample() {
DrawWithCacheExample()
}
Explanation:
drawWithCache
calculates the circle radius once and caches it. TheonDrawWithContent
lambda then uses the cached radius to draw a red circle over the existing content. This is efficient for static drawing instructions.
Step 4: Transformations
You can apply transformations like rotation, scale, and translation within the DrawScope
:
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
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.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun TransformationExample() {
Canvas(
modifier = Modifier.size(200.dp),
onDraw = {
translate(left = size.width / 4, top = size.height / 4) {
rotate(degrees = 45f) {
scale(scaleX = 0.7f, scaleY = 0.7f) {
drawRect(
color = Color.Magenta,
topLeft = Offset(0f, 0f),
size = size / 2f
)
}
}
}
}
)
}
@Preview(showBackground = true)
@Composable
fun PreviewTransformationExample() {
TransformationExample()
}
Explanation:
translate
moves the drawing origin.rotate
rotates the drawing.scale
scales the drawing.- We’re drawing a magenta rectangle after applying these transformations.
Step 5: Paths
For complex shapes, use Path
:
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
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.Fill
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun PathExample() {
Canvas(
modifier = Modifier.size(200.dp),
onDraw = {
val path = Path().apply {
moveTo(size.width / 2, 0f)
lineTo(size.width, size.height / 2)
lineTo(size.width / 2, size.height)
lineTo(0f, size.height / 2)
close()
}
drawPath(
path = path,
color = Color.DarkGray,
style = Fill
)
}
)
}
@Preview(showBackground = true)
@Composable
fun PreviewPathExample() {
PathExample()
}
Explanation:
- We create a
Path
and define its points. moveTo
sets the starting point of the path.lineTo
adds a line to the path.close
closes the path, creating a closed shape.drawPath
draws the defined path with a gray color.
Best Practices for Custom Drawing
- Optimize Performance: Use
drawWithCache
for static drawing to avoid redundant calculations. - Use Transformations Wisely: Minimize complex transformations to avoid performance issues.
- Keep it Modular: Break down complex drawings into smaller, reusable functions.
- Use Vector Graphics: For resolution-independent graphics, consider using vector-based drawing techniques.
Conclusion
Custom drawing in Jetpack Compose allows you to create unique and visually appealing UI elements beyond what the standard library provides. By understanding the use of Canvas
, drawBehind
, drawWithContent
, drawWithCache
, transformations, and paths, you can unleash your creativity and design truly custom UIs. Optimize your drawing operations and follow best practices to ensure your app remains performant and visually stunning.