Custom Shape with Jetpack Compose

After exploring the Jetpack Compose Canvas in a previous article, this new post will explain how you can draw and use a custom Shape for your Composables to give them a specific outline. The Jetpack Compose foundation library already provides common shapes but we’ll see how to take advantage of the Path API to build custom outlines. We will go even further by adding some animations to our brand new shapes.

Introduction to Shape

With Jetpack Compose, it is easy to add some rounded or cut corners to your Composable thanks to the foundation library which provides several shapes:

  • RoundedCornerShape(): to have a rounded corner outline with a given radius

For example, a CutCornerShape with a corner size of 32dp applied to a Text Composable looks like the following image:

CutCornerShape

Shapes can be applied to your Composable thanks to by using several Modifier functions. Let’s have a look at them:

  • background(color, shape): it will define the new outline of the Composable with a given Color or Brush.

We can combine multiple Modifier methods to get the shape you can see above. The order matters when chaining the Modifier functions! For example, be sure to always call background() after graphicsLayer{} to ensure that your Composable will be correctly painted with the right color. Here is the code snippet of the Composable.

Now that we’ve had a glimpse at the capabilities of shapes and how to use them, let’s see how we can build our own custom shapes.

Drawing custom shapes

Jetpack Compose offers us two options: going for the GenericShape class and provide a custom Path in the constructor builder or we can create a new class that extends the Shape interface where we will need to override the createOutline() method.

First, let’s see how we can use the Shape interface in order to build a shape that looks like a cinema ticket.

Cinema ticket shape

Then, let’s have a look at the code of the path which will define the outline of the Composable. The path will be composed of 4 corners and 4 lines. We’ll start with the top left corner and will turn clockwise to close the path.

For the corners, the Path API offers a function arcTo() to achieve this type of form that we want for the ticket shape. This method draws an arc that follows the edges of a given rectangle (Rect) from a start angle (the origin is located at the right hand side in the middle of the rectangle) and a sweep angle going clockwise around the oval to define the arc.

Then, to draw the edges of the shape, we’ll use the lineTo() method, which accepts the (x;y) coordinates of the destination point. This will help us to join the corners together.

Once the path completed, we can call the close() method in order to end the path we just built. This function draws a straight line to the first point of the path to close it.

Now that the shape is ready, let’s use it to build our cinema ticket. In the Composable Modifier, we are going to set the shape in the graphicsLayer{} lambda in order to have access to the elevation and clip attributes. Then, we’ll apply the color background with background() (you can apply a Brush if you want a fancy gradient).

In order to draw the inner border, we are going to use the same path function in the drawBehind{} lambda. It must be called after the graphicsLayer{} lambda and the background() function to draw the border on top the shape. Thanks to the DrawScope, we are able to draw same path with the drawPath() method set with a stroke style and a dash path effect. In the end, we’ll apply a scale transformation to have the inner border we want.

Tada 🎉 Here is our brand new movie ticket!

Ready to go to the theater?

This is not over! We can do much more with the shapes by adding some fancy animations. Let’s see how we can build dynamic and animated shape in the next section.

Animate custom shapes

In this section, we are going to see how to use the animation API to animate the shape. For this sample, we’ll use the rememberInfiniteTransition() but you can play any kind of animation API here. For the first shape, we are going to animate the top of the shape with a wave movement and for the second shape we are going to make a new polygon, adding a new side every iteration. The dynamic shape will be applied to an Image in the graphicsLayer{} lambda.

Here is the code snippet of the Composable with an animated wave shape:

And the following code snippet for the dynamic polygon shape:

Let’s focus on the shape instances. On the first one, we have to pass the dx value to the shape in order to display a dynamic outline over time. On the second, we have to pass the number of polygon edges to the path.

As you can see in first Composable, we have to store the value of the animation in an intermediate variable in order to dispatch the animation value to the path. Without this trick the animation will not be triggered. Whereas in the second Composable, we can pass the value directly to the constructor. Let’s see why!

Shapes were designed to be immutable, meaning that the constructed Shape instance should always be returning the same Outline for the same input size. If the Shape is dynamic and uses the dynamic property inside the outline creation lambda, the animation won’t be animated. But if we are using the dynamic MutableState property inside the layer builder lambda, when the state changes, the block is reexecuted. So during the execution we are recreating a new immutable Shape.

For animating shapes, I would recommend to build your dynamic shapes by extending the Shape interface and passing the animation value to the constructor as you can see in the following code snippet.

And here is what it looks like when we are running the samples:

Dynamic and animated shapes

Mastering the Path API will allow you to build great custom shapes for your Composable. And you will be able to go beyond and add animation to your shapes in order to add motion to your apps. Feel free to check this great (old but gold) post by Nick Butcher about the Path API.

Big thanks to Joe Birch and Corentin Evanno for the review 👏🙂

Do not hesitate to ping me on Twitter if you have any question 🤓

Google Developer Expert for Android — Senior Android Engineer @ Aircall (Paris) — Startup way of life, beer lover and world traveler.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store