Graphics Using Jetpack Compose | Kodeco


Android has a variety of graphics-based objects to place on the app’s screen. Some of these graphics objects include text, buttons and checkboxes. What if you wanted to draw custom graphics? Like rectangles, lines, triangles and other shapes? Video games, painting apps or chart drawing programs need custom graphics. When you want a nice avatar for your next game, custom graphics with Jetpack Compose will be the way to create it.

You draw custom graphics on a special view called a Canvas with a Paint interface. When working with Jetpack Compose, you use the Graphics API. The Graphics API uses an approach called the declarative programming paradigm. In this tutorial, you’ll learn this simpler way to use the Graphics API on Canvas.

In this tutorial, you’ll use Jetpack Compose to:

  • Draw primitive shapes with custom graphics.
  • Create complex custom graphics by combining graphics objects.
  • Display text using Paint.
  • Transform objects.
Note: This tutorial assumes you have experience with Android and Kotlin. If that’s not the case, check out the Beginning Android Development with Kotlin series and other Kotlin and Android tutorials to get familiarized first.

Getting Started

Start by using the Download Materials button at the top or bottom of this tutorial to download the project.

Open the project in Android Studio Bumblebee or later and get familiar with the files. You’ll notice a starter project and a final project. Open and run the starter project. The app – Pacman – contains a blank, white screen.

Humble beginnings so far, right? If you’re wondering about the end result of the project, open and run the final project. You’ll see the Pacman screen:

Full Pacman App custom graphics with Jetpack Compose

Hopefully, seeing the final app will get you fired up to start drawing some awesome Pacman graphics. So with that, waka, waka, chomp, chomp. Time to get to it!

Creating Jetpack Compose Custom Graphics

You draw graphics like buttons, text fields and pickers by placing them on a view. “Graphics” in this tutorial refers to custom drawings like rectangles, triangles and lines. The graphics here, called primitives, aren’t like sophisticated shapes like buttons. You could create graphics using bitmap or SVG, but the Graphics API in Android gives you an alternative way to do it. Instead of drawing graphics using tools like Adobe Illustrator and importing them as bitmap or SVG, you can create raw graphics directly through code.

Using only code, you still create a bitmap with pixels and will see the same images on the Android screen. Your code uses Canvas to draw objects using a bitmap. Instead of putting certain colors in specific x and y locations, you use helper methods to draw common shapes like lines, rectangles and circles. Finally, to change the bitmap’s style and colors, you use the Paint interface.

Using Declarative Graphics API

The Graphics API has been in the Android SDK since its inception, API level 1, in 2008. What’s new is a declarative way to use this API: It makes managing Canvas and Paint an easy task. You don’t need to set method or other configurations on the Paint object. Instead, all the configuration and execution happen in one place: the composable function. Before Jetpack Compose, working with the Paint API required meticulous detail since code organization decisions could cause noticeable performance inefficiencies. Additionally, working with Canvas can be confusing. But with Compose, it’s a breeze to create graphics.

Understanding Canvas

What is Canvas?

To draw an object on the Android screen, first you need a Drawable to hold your drawing. Drawings are composed of pixels. If you’ve worked in Android very long, you’ll know about Drawable. You use this class if you want to display an image. Canvas holds the methods to draw shapes. So, with Drawable, you override the draw method. The draw method accepts Canvas as the argument. This connection allows you to draw shapes using code in Drawable to put them on the Canvas.

What can you do with Canvas?

Canvas allows you to draw many primitive shapes, from circles to clipping the shapes. You could say that Canvas is an abstraction of Drawable. You load up an image with Drawable by inflating SVG or PNG files. Inflating doesn’t need Canvas. But if you wanted to draw a primitive shape dynamically, you’d use Canvas.

Think of Canvas as a layer on top of Drawable where you could draw a variety of shapes.

Creating a Canvas

Creating a Canvas is straightforward. In the starter project, open MainActivity.kt and check out CanvasApp:

  Canvas(modifier = Modifier
  ) {

Canvas accepts a modifier that lets you modify the Canvas’s size, for example. In this example, you set the size to the maximum size of the parent.

When you ran the starter project, you didn’t see anything, but it’s time to change that. Look at the first two lines inside the Canvas block:

    val canvasWidth = size.width
    val canvasHeight = size.height

Inside the Canvas block, you could query the size of Canvas from size. Remember the modifier above that extends to the maximum width and height? That’s the size of Canvas.

Drawing on a Canvas With Jetpack Compose

Following the first two lines is a call to drawBlackScreen:

    drawBlackScreen(scope = this, canvasWidth, canvasHeight)

Go inside drawBlackScreen. It’s empty right now. Put the following code inside it:

    size=Size(canvasWidth, canvasHeight))

As its name suggests, drawRect draws a rectangle. It’s worth noting that thoughtful method names are a tremendous help in code development. You know what drawRect does by its name: It draws a rectangle. But what are its parameters? The first is the color of the rectangle and the second parameter is the size of the rectangle.

By calling Size, Android Studio assists you in adding the required import, androidx.compose.ui.geometry.Size.

Build and run the app. You’ll see a black rectangle over the full screen:

Black rectangle on Pacman app, custom graphics with Jetpack Compose

In your usage, you omitted the Paint object argument, but you could supply one to change the style of the drawn rectangle. You’ll see how to construct Paint later. You also omitted the position argument. This means you used the default value for the position, which is 0 for x and y coordinates. Other parameters define color and size, which are common to all objects. Other object methods require different parameters according to the object shape. drawCircle needs an argument for radius, but the line object doesn’t.

Using Modifier

You’ve seen Modifier in the Canvas function. But you have other arsenals as well. Suppose you want to add some padding. Change the Modifier inside Canvas to:

  Canvas(modifier = Modifier
  ) {

Don’t forget to import padding. Each method on the modifier returns an updated Modifier instance. So by chaining the method calls, you’re gradually building the Canvas. The order by which you call the methods matter.

Rebuild the project and run it. You’ll see the black screen now has white padding:
Black rectangle with white padding

Now that you’ve seen how padding works for custom graphics in Jetpack Compose, you can remove that code.

Creating Objects With Jetpack Compose

It’s finally time to try to draw some shapes! You’ll see that each of these shapes has its own characteristics.

Drawing Lines

Now that you’ve created a rectangle, it’s time to create other shapes. You’ll start by drawing part of the Pacman maze. Go inside drawBlueLines. You notice that there’s an annotation on top of this method, @OptIn(ExperimentalGraphicsApi::class). It’s needed because you use Color.hsv, which is experimental. So what is this method? It gets a color that you’ll use to draw on Canvas. HSV (hue, saturation, value) is one of the color spaces besides RGB (red, green, blue). You can read more about HSV in Image Processing in iOS Part 1: Raw Bitmap ModificationColor.hsv accepts three arguments: hue, saturation and value. The saturation and value ranges from 0 to 1. It’s a float which represents the percentage value.

In this method, you need to draw four lines. You’ve already got the positions defined for you.

Add the following code at // 2. Use the drawLine method:

      blue, // 1
      Offset(0f, line), // 2
      Offset(canvasWidth, line), // 2
      strokeWidth = 8.dp.value // 3

Here’s what is happening:

  1. blue is the color you got from Color.hsv.
  2. These define the dot positions that make a line when connected. Each dot needs an Offset. Basically, it’s an object that accepts two values, the x and y positions.
  3. This sets the width of the stroke. The higher the value, the thicker your line becomes. You define a line by two points. That’s why the method to draw a line needs two Offset arguments. It’s different from the method for drawing a rectangle.

Rebuild the project and run the app. You’ll see four blue lines:

Four blue lines, custom graphics with Jetpack Compose

Notice that you’ve drawn lines after drawing a rectangle — the order matters. If you draw lines first, then draw a rectangle, the big rectangle will cover your lines.

Drawing Circles

Next, you’ll draw a power pellet. The circle represents an object that, if eaten by Pacman, makes him immune to ghosts for a certain period of time. Go to // 3. Use the drawCircle method, and add the following:

  scope.drawCircle(purple, // 1
    center = Offset(pacmanOffset.x + 600.dp.value, dotYPos), // 2
    radius = radius) // 3

Here’s what this code does:

  1. This specifies the color of the circle.
  2. The center argument refers to the position of the center of the circle in Canvas.
  3. The radius refers to how big your circle is.

As you can see, both methods — whether drawing a rectangle, line or circle — accept a color argument. However, they differ in other arguments because every shape is a bit different. All the methods accept an optional Paint object.

Build the project and run the app. You’ll see a purple circle:

Purple circle, custom graphics with Jetpack Compose

With that, your power pellet is ready to give ol’ Blinky — spoiler — a run for his money! :]

Drawing Point Lines

In the Pacman video games, this line of points refer to the dots that Pacman needs to eat to finish the game. You can create all the points one by one, but you could also use a method to create a line consisting of points. Find // 4. Use the drawPoints method, and add the following:

  scope.drawPoints(points, // 1
    PointMode.Points, // 2
    purple, // 3
    strokeWidth = 16.dp.value) // 4

This code defines:

  1. The list of Offsets where you defined the position of points.
  2. The mode or style of the point. Here, you render small squares. There are other PointMode options. Try them out before moving on. Press Ctrl (or CMD on a Mac) + Space on your keyboard to see the other options.
  3. Color.
  4. Line thickness.

Build the project and run the app. You’ll see a line of points:

A Line of points, custom graphics with Jetpack Compose

Drawing Arcs

Now, here comes the most exciting part of the tutorial: drawing Pacman himself! Pacman is a not-quite-full circle. You call this shape a sector. You call a quarter of a circle an arc. Pacman looks like a circle with an arc taken out!

Below // 5. Use the drawArc method within drawPacman, add the following code:

    Color.Yellow, // 1
    45f, // 2
    270f, // 3
    true, // 4
    Size(200.dp.value, 200.dp.value)

This code specifies:

  1. Yellow as the arc’s color.
  2. Start angle, which refers to the bottom part of Pacman’s mouth.
  3. Sweep angle. Sum the start angle and the sweep angle, and you’ll get the position of the top part of the mouth. Zero degrees starts at the right side of the circle. If you think of the top as north and bottom as south, then zero degrees is in the west direction. You could change the start angle to zero and redraw Pacman to see the location of zero degrees.
  4. Whether you draw a line between the start angle and the end angle using the center. If not, you draw a direct line between the start and end angles. In your case, you want to use the center because you create a mouth by making a line from the start angle to the center and then from the center to the end angle.

Build the project and run the app. You’ll see Pacman:

Pacman eating dots, custom graphics with Jetpack Compose

You can see the difference between using the center or not in drawing an arc in the picture below:

Using the center in drawing an arc, custom graphics with Jetpack Compose

Drawing Complex Shapes: Blinky the Ghost

The ghosts that chase your Pacman have a complex shape, and Jetpack Compose doesn’t have any “ghost” shapes in its custom graphics. :] So, you’ll draw a custom ghost shape. To do this, you need to divide a ghost into a few simple shapes and draw them each using the methods you’ve learned.

You can separate a ghost into different primitive shapes:

Ghost Dissected, custom graphics with jetpack compose

Drawing the Ghost’s Feet

Breaking down the ghost custom graphic, separate the feet. What do you see? Three arcs or half-circles lined up horizontally.

Go inside drawGhost and add the following code:

    val ghostXPos = canvasWidth / 4
    val ghostYPos = canvasHeight / 2
    val threeBumpsPath = Path().let {
      it.arcTo( // 1
        Rect(Offset(ghostXPos - 50.dp.value, ghostYPos + 175.dp.value),
          Size(50.dp.value, 50.dp.value)),
        startAngleDegrees = 0f,
        sweepAngleDegrees = 180f,
        forceMoveTo = true
      it.arcTo( // 2
        Rect(Offset(ghostXPos - 100.dp.value, ghostYPos + 175.dp.value),
          Size(50.dp.value, 50.dp.value)),
        startAngleDegrees = 0f,
        sweepAngleDegrees = 180f,
        forceMoveTo = true
      it.arcTo( // 3
        Rect(Offset(ghostXPos - 150.dp.value, ghostYPos + 175.dp.value),
          Size(50.dp.value, 50.dp.value)),
        startAngleDegrees = 0f,
        sweepAngleDegrees = 180f,
        forceMoveTo = true
    scope.drawPath( // 4
      path = threeBumpsPath,
      style = Fill

By calling Rect, Android Studio assists you in adding the required import, androidx.compose.ui.geometry.Rect.

  1. arcTo is similar to drawArc above: the startAngleDegrees and sweepAngleDegrees arguments are like start and sweep angles where the first argument is the rectangle that defines or bounds the size of the arc. The last argument moves the Path point to the end of the path before drawing another arc. Otherwise, you’d always draw other arcs from the same starting position or beginning of the first arc.
  2. You did exactly the same as above, only you’re starting at the end of the first one.
  3. For the last leg, you start at the end of the second leg.
  4. path argument is the path you’ve created, and the second argument is the color of your path. The third argument, fill, is whether you should fill the path with the selected color.

Rebuild the project and run the app. You’ll see the ghost’s feet:

Ghost's feet, custom graphics with Jetpack Compose

Note: Instead of using drawPath, you could use drawArc three times. Experiment with drawArc and see which one is more convenient for you.

Drawing the Ghost’s Body

Now, you’ll draw a rectangle as the main part of the ghost’s body. You already know how to build a rectangle, so add the following code at the bottom of drawGhost:

      Offset(ghostXPos - 150.dp.value, ghostYPos + 120.dp.value),
      Size(150.dp.value, 82.dp.value)

Rebuild the project and launch the app. You’ll see the ghost’s body:

Ghost's body, custom graphics with Jetpack Compose

A ghost with a body? Only in Pacman. :]

Drawing the Ghost’s Head

The ghost’s head is a half-circle arc, but bigger and in the opposite direction of the ghost’s feet. Add the following code at the bottom of drawGhost:

      startAngle = 180f,
      sweepAngle = 180f,
      useCenter = false,
      topLeft = Offset(ghostXPos - 150.dp.value, ghostYPos + 50.dp.value),
      size = Size(150.dp.value, 150.dp.value)

Starting at the top left corner of the ghost’s body, you draw an arc 180 degrees to the right. Rebuild the project and run the app. You’ll see the ghost’s head:

Ghost's full body, custom graphics with Jetpack Compose

Drawing the Ghost’s Eyes

Wow, all you’re missing now are the eyes! The ghost has two eyes, with each eye composed of a white outer circle and a black inner circle for the iris. So now, you’ll draw four circles just like you’ve already done with the power pellet. Add the following code at the bottom of drawGhost:

      center = Offset(ghostXPos - 100.dp.value, ghostYPos + 100.dp.value),
      radius = 20f
      center = Offset(ghostXPos - 90.dp.value, ghostYPos + 100.dp.value),
      radius = 10f

Rebuild the project and run the app. You’ll see a one-eyed ghost:

One-eyed ghost, custom graphics with Jetpack Compose

Now, try to draw the ghost’s left eye. Gotta have two eyes to catch Pacman. :]

Drawing Text With Jetpack Compose

To draw text, you need to access the native Canvas object because you can’t draw text on top of Jetpack Compose’s Canvas. Inside drawScore, you’ll see that you have textPaint:

  val textPaint = Paint().asFrameworkPaint().apply {
    isAntiAlias = true
    textSize = 80.sp.value
    color =
    typeface = Typeface.create(Typeface.MONOSPACE, Typeface.BOLD)
    textAlign =

Text is drawn as a custom graphic using Jetpack Compose with Paint. You change the style and color of the text through this interface. Normally with Canvas, you use the base Paint object, but because you’re using the native Canvas object, you need the framework Paint method called asFrameworkPaint. Inside the asFrameworkPaint.apply block above, you configure the Paint object’s text for font, style, size and color.

Additionally, there’s no drawText inside the DrawingScope for the normal Canvas object. You need to call into the nativeCanvas interface to access its drawText method. To draw text on the native Canvas, add the following code below // 7. Draw a text:

  scope.drawIntoCanvas {
    it.nativeCanvas.drawText( // 1
      "HIGH SCORE", // 2
      canvasWidth / 2, // 3
      canvasHeight / 3, // 3
      textPaint // 4
    it.nativeCanvas.drawText( // 1
      "360", // 2
      canvasWidth / 2, // 3
      canvasHeight / 3 + 100.dp.value, // 3
      textPaint // 4

Here’s what’s happening:

  1. Like the Path in arcTo earlier, it is the Canvas object inside the drawIntoCanvas block. You reference the native Canvas object and then use drawText to draw the text.
  2. This is the text you want to write.
  3. These are the x and y coordinates for text placement.
  4. This is the framework Paint object representing the text’s color, size and font.

Build the project and run the app. You’ll see the following text on the screen:

Score text, custom graphics with Jetpack Compose

Scaling, Translating and Rotating Objects With Jetpack Compose

You’ve done a lot of drawing! Sometimes, after drawing objects, you might need to transform them. For example, you might want to make them bigger or change their position or direction. You may also need to rotate Pacman as he moves through the maze: When he moves north, his mouth should point north.

Scaling and Translating Objects

Look at the ghost, and you’ll see he’s smaller than Pacman. Also, the ghost’s position is slightly lower than Pacman’s. You can fix this by transforming the ghost, which you’ll do now.

DrawingScope has the withTransform method. Inside this method, add the scale and translate modifiers. Inside drawGhost wrap every code there with the following snippet below:

  scope.withTransform({ // 1
    scale(1.2f) // 2
    translate(top=-50.dp.value, left=50.dp.value) // 3
  }) {

Here’s what this code does:

  1. scope block uses the method withTransform. Inside the block, withTransform uses two methods, or modifiers, to transform the object.
  2. scale changes the size of the object. The argument 1.2f means the object will be 20% bigger.
  3. translate has two arguments, top and left. top changes the vertical position of the object. The negative value, -50.dp.value, means the object rises upward. A positive value pushes the object downward. The horizontal position of the object changes with the left argument. The negative value, -50.dp.value, means object moves to the left. A positive value would move the object to the right.

Build the project and run the app. You’ll see the ghost has moved slightly up and become bigger:

Bigger ghost, custom graphics with Jetpack Compose

Well, look at that! A Blinky replica. Blinky would be proud. Waka do you think? :]

Rotating Objects

The Pacman game has a lives indicator, and adding one will make this complete. The indicator is the Pacman shape itself. So, if the indicator has three Pacman shapes, it means you have three lives. You already know how to draw Pacman. Now, you need to duplicate him. But that’s not enough. You also need to move the clones, make them smaller, and then rotate them.

Go into drawPacmanLives, and add the following code:

    translate(top=1200.dp.value, left=-1050.dp.value)
  }) {
      Size(200.dp.value, 200.dp.value)

You’ve seen the drawArc code before. It’s the code you used when you drew the original Pacman. withTransform will scale, translate, and finally, rotate Pacman. rotate accepts the angle argument in degrees.

Build the project and run the app. You’ll see another Pacman but smaller and in a different place:

One Life of Pacman, custom graphics with Jetpack Compose

Now, try to draw another Pacman beside this clone to indicate that you have two lives left in the game.

Where to Go From Here?

Download the final project by clicking Download Materials at the top or bottom of this tutorial.

You’ve learned about the Graphics API in Jetpack Compose. You learned how to set up Canvas and modify it using a modifier. You’ve drawn many shapes and transformed them. You also used the native Canvas to draw text on it.

But there are more things you haven’t explored, such as animation. You could move Pacman and make his mouth open to simulate eating the dots. Of course, you could give life to the ghost, make it flash blue, and have it chase Pacman.

Feel free to checkout this wonderful Getting Started with Jetpack Compose Animations tutorial followed by the more advanced Jetpack Compose Animations tutorial.

We hope you enjoyed this tutorial! If you have any comments or questions, please join the forum discussion below.


Source link