DEV Community

Cover image for Using shapes in Jetpack Compose
Thomas Künneth
Thomas Künneth

Posted on • Updated on

Using shapes in Jetpack Compose

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:

Three filled rectangles

@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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
      }
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

Three filled circles

@Composable
fun Circle(color: Color,
      modifier: Modifier = Modifier) {
  Box(
      modifier = modifier.composed {
        preferredSize(32.dp)
            .clip(CircleShape)
            .background(color)
      }
  )
}
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

it produces this output:

Three rectangles with cut corners

But what about shapes that are not covered by builtin code? In such cases you can use GenericShape. Take a look:

A yellow triangle

@Composable
fun RowWithATriangle() {
  Row() {
    Triangle(modifier = Modifier.fillMaxWidth(),
        color = Color.Yellow)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
      }
  )
}
Enter fullscreen mode Exit fullscreen mode

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:

Yellow shape with four corners

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.

Source code of class GenericShape

So, GenericShape implements createOutline(), which is declared in Shape, by

  • applying the provided drawing instructions to a Path instance
  • calling close()
  • returning an instance of Outline.Generic

Let's see where the implementation of Path comes from.

Actual implementation of Path()

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

A red donut

I use two composables:

@Composable
fun RowWithADonut() {
  Row(
      modifier = Modifier.fillMaxWidth(),
      horizontalArrangement = Arrangement.Center) {
    Donut()
  }
}
Enter fullscreen mode Exit fullscreen mode
@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)
        }
      }
  ) {
  }
}
Enter fullscreen mode Exit fullscreen mode

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.


source code

Discussion (1)

Collapse
tkuenneth profile image
Thomas Künneth Author • Edited on

I continue with this topic in a small series called Drawing and painting in Compose