Table Of Contents:
Oftentimes, we get asked to create different custom views and shapes in Android Development. In the old view-based system, we heavily relied on Canvas to create a shape. Luckily, Jetpack Compose introduces new out-of-the-box ways to easily create and modify shapes on the fly.
This post will walk you through the available standard shapes in Compose Foundation and how you can leverage them for your needs or create custom ones.
What is Shape?
In Jetpack compose Shape
is an interface to define the form, contours, or outline of an object.
Two classes implement the Shape
interface:
CornerBasedShape
creates a rectangle defined by fourCornerSize
s.CornerSize
can accept dp, px, and percent values.CornerBasedShape
is a parent class to four shapes:RoundedCornerShape
andAbsoluteRoundedCornerShape
are used to define a rectangle with rounded corners.RoundedCornerShape
is layout-direction aware(LayoutDirection.Rtl
) and automatically mirrors the corner sizes.
In the example above, the same parameters for
RoundedCornerShape
returned a different shape based on the system’s layout direction.If you’d like to disregard it - use
AbsoluteRoundedCornerShape
.CutCornerShape
andAbsoluteCutCornerShape
both define a rectangle with cut corners. Similar toRoundedCornerShape
,CutCornerShape
is layout-direction aware and automatically mirrors the corner sizes. UseAbsoluteCutCornerShape
if layout direction needs to be disregarded.
GenericShape
creates a custom shape by applying the provided builder on aPath
. More details are below.
The shape component also comes with two top-level properties, that don’t require any parameters:
RectangleShape
to apply a rectangle form;CircleShape
to apply a circle form.
1val circleShape = CircleShape
2val rectangleShape = RectangleShape
How to apply Shape?
The shape can be applied to most Compose Elements and Layouts via Modifier
. Several Modifier
functions accept shape:
clip(shape)
is an extension function, that will clip content to a selected shape. Under the hood, it hitsgraphicsLayer()
function.graphicsLayer()
is a function, that applies different effects to content such as rotation, scaling, clipping, and shadow.background(color, shape)
is a function, that will apply the selected shape to the background and won’t affect the content.border(width, color, shape)
is a function, that will apply the selected shape to the border.drawBehind()
is a function that will draw the selected path behind the content. This function does not exactly acceptShape
class, however, it provides a stateless APIDrawScope
to draw shapes and paths without requiring developers to maintainCanvas
state. You can pass thePath
of a shape todrawPath()
function inside.
You can also combine any of the above functions simultaneously to get the most desired outcome. For example, to re-create a CircularImageView of the old view-based system in Jetpack Compose, you can leverage graphicsLayer
and border
functions:
1val circleShape = CircleShape
2 Image(
3 painter = painterResource(id = R.drawable.jetpack_compose_logo),
4 contentDescription = "Jetpack Compose Logo",
5 modifier = Modifier
6 .size(150.dp)
7 .graphicsLayer { // call this function to apply custom shadow elevation, otherwise use `clip()`
8 shadowElevation = Dimensions.medium.toPx() //your custom shadow elevation in px
9 clip = true //make sure to set clip to true
10 shape = circleShape
11 }
12 .background(
13 color = colorResource(id = R.color.cupsOfCode_dark_green)
14 )
15 .border(
16 border = BorderStroke(
17 width = 3.dp,
18 color = colorResource(id = R.color.cupsofCode_brown)
19 ), shape = circleShape
20 )
21
22 )
The result is:
Create a custom shape
To create a custom shape, you can:
- a) create a custom class by implementing the
Shape
interface and overriding itscreateOutline
function
or
- b) use
GenericShape
and pass a builder lambda to it. Under the hood,GenericShape
overrides thecreateOutline
function ofShape
.
For example, to create a heart, I’ve converted this logic
from StackOverFlow into an extension function heartPath()
of Path
(ext function is here
). Then you can use the first option and create a custom class Heart
:
1class Heart: Shape {
2 override fun createOutline(
3 size: Size,
4 layoutDirection: LayoutDirection,
5 density: Density
6 ): Outline {
7 val path = Path().apply {
8 heartPath(size = size)
9 close()
10 }
11 return Outline.Generic(path)
12 }
13}
Or use the second option with GenericShape
:
1@Composable
2fun heart(): GenericShape {
3 return GenericShape { size, _ ->
4 heartPath(size = size)
5 }
6}
Both options will return a heart shape:
Note: when using
GenericShape
you don’t need to callclose()
function of thePath
, because it’s already taken care of.
Final Thoughts
Shape API in Jetpack Compose provides quick solutions for complex UI elements. Unlike the old view-based system, Shape API doesn’t require additional vector XML files or deep-dive access to Canvas
and the customization of most components can be done directly on them via available Modifier
functions. So, give it a try and spend some time playing with it.
Thanks for reading! Let me know in the comments if you have any questions.