In this article I will take a closer look at how to achieve custom drawing in Jetpack Compose using androidx.compose.ui.graphics.Shape. Let's start with something really simple:
@Composable
fun RowWithThreeRectangles() {
Row(modifier = Modifier.fillMaxWidth()
) {
Rectangle(color = Color.Red)
Rectangle(modifier = Modifier.weight(0.3f)
.height(64.dp),
color = Color.Blue)
Rectangle(modifier = Modifier.weight(0.7f),
color = Color.Green)
}
}
Rectangle receives a color and an optional modifier (which I use to control the size of the rectangle).
@Composable
fun Rectangle(color: Color,
modifier: Modifier = Modifier) {
Box(
modifier = modifier.composed {
preferredSize(32.dp)
.clip(RectangleShape)
.background(color)
}
)
}
It is just a Box which is clipped to a shape. clip() gets an instance of androidx.compose.ui.graphics.Shape. This interface has just one funtion, createOutline(), which receives two parameters:
- the size of the shape boundary
- the current density of the screen
It returns an instance of androidx.compose.ui.graphics.Outline. This class defines a (more or less) simple shape, which is used for bounding graphical regions by
- defining a shape of the component background or a shape of shadows cast by the component
- clipping the contents
I will cover Outline later. For now I stick to existing implementations of Shape.
@Composable
fun RowWithThreeCircles() {
Row(modifier = Modifier.fillMaxWidth()
) {
Circle(color = Color.Red)
Circle(color = Color.Blue)
Circle(color = Color.Green)
}
}
@Composable
fun Circle(color: Color,
modifier: Modifier = Modifier) {
Box(
modifier = modifier.composed {
preferredSize(32.dp)
.clip(CircleShape)
.background(color)
}
)
}
The code is practically the same, only difference being that CircleShape is used. In your app you would probably refactor this into one function and just pass the Shape. CircleShape, by the way, is not a class but a variable. Its type is RoundedCornerShape, which extends abstract CornerBasedShape. Another child of this class is CutCornerShape. It produces a rectangle with cut corners. If you edit my Rectangle like this
.clip(CutCornerShape(8.dp))
it produces this output:
But what about shapes that are not covered by builtin code? In such cases you can use GenericShape. Take a look:
@Composable
fun RowWithATriangle() {
Row() {
Triangle(modifier = Modifier.fillMaxWidth(),
color = Color.Yellow)
}
}
I want the triangle to consume the full width, but leave its height at its preferred value.
@Composable
fun Triangle(color: Color,
modifier: Modifier = Modifier) {
Box(
modifier = modifier.composed {
preferredSize(64.dp)
.clip(GenericShape { size ->
moveTo(size.width / 2, 0f)
lineTo(size.width - 1, size.height - 1)
relativeLineTo(-size.width, 0f)
})
.background(color)
}
)
}
size is the size of the composable the shape is applied to. You can access its width with size.width and the height with size.height. The shape is defined through some basic instructions. Initially painting will start at the top left corner. moveTo() sets the current location of the pencil, whereas lineTo() draws a line from that positon to a new one. Did you notice that I don't move back to the starting point? As you shall see shortly this is done automatically by invoking a function named close(). You can check this if you add something like relativeLineTo(32f, -32f). In this case we no longer have a triangle, but something that looks like this:
Where do these drawing primitives (and close()) come from? They are defined in the interface androidx.compose.ui.graphics.Path. To see how it is used by Compose itself let's peek into GenericShape.
So, GenericShape implements createOutline(), which is declared in Shape, by
- applying the provided drawing instructions to a
Pathinstance - calling
close() - returning an instance of
Outline.Generic
Let's see where the implementation of Path comes from.
Finally, what about Outline? Basically, it wraps a Path. It is not instantiated directly. Instead, you should use one of its nested classes Rectangle, Rounded or Generic. Let's see how this works. To get this
I use two composables:
@Composable
fun RowWithADonut() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center) {
Donut()
}
}
@Composable
fun Donut() {
Surface(modifier = Modifier.preferredSize(100.dp),
color = Color.Red,
shape = object : Shape {
override fun createOutline(size: Size, density: Density): Outline {
val thickness = size.height / 4
val p1 = Path().apply {
addOval(Rect(0f, 0f, size.width - 1, size.height - 1))
}
val p2 = Path().apply {
addOval(Rect(thickness,
thickness,
size.width - 1 - thickness,
size.height - 1 - thickness))
}
val p3 = Path()
p3.op(p1, p2, PathOperation.difference)
return Outline.Generic(p3)
}
}
) {
}
}
Let's see what is worth mentioning here. First of all I use Surface, which gets the shape from shape. The color is set with, well, color. For the sake of simplicity I construct the shape in place. In your app you probably want to wrap it in a class, for example DonutShape. createOutline() does quite a few things. It creates three paths, one large and a smaller oval. The third path is the result of the difference of them. Think of it as cookie cutting. I remove the smaller one from the larger, which gives us a donut-like shape.
Have you worked with shapes and outlines, too? Please share your thoughts in the comments.








Top comments (1)
I continue with this topic in a small series called Drawing and painting in Compose