Drawing Custom Shapes in Android

Learn how to draw custom shapes and paths in Android by creating a neat curved profile card with gradient colors. By Ahmed Tarek.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Using CustomPainter

Now that you’ve learned some theory, it’s time to start using the Android Canvas and add some code that will reproduce your drawing in the app.

Implementing the Painter Interface

Start by creating a new class ProfileCardPainter in the starsofscience package. Then replace the whole file content with:

package com.raywenderlich.android.starsofscience

import android.graphics.*
import androidx.annotation.ColorInt

//1
class ProfileCardPainter(
    //2
    @ColorInt private val color: Int
) : Painter {
  //3
  override fun paint(canvas: Canvas) {

  }
}

Here you:

You’ll write all your drawing code inside this function, which gives you one parameter: The canvas to draw on.

  1. Define a new class named ProfileCardPainter that implements the interface Painter.
  2. Then in its primary constructor you define the profile color as a class property.
  3. Finally, you implement paint(canvas: Canvas). CustomPainter will call this method whenever the object needs to paint.

Rendering With CustomPainter

Go to MainActivity.kt. You’ll find the following line of code in onCreate():

profileCardContainer.setBackgroundColor(R.color.colorPrimary.toColorInt(this))

It sets a background color to the profileCardContainer which is a FrameLayout already defined in XML. You don’t need that line anymore because you want to add your custom shape instead of that solid color.

Replace that line with the following code:

//1
val azureColor = R.color.colorPrimary.toColorInt(this)
val avatarRadius = R.dimen.avatar_radius.resToPx(this)
val avatarMargin = R.dimen.avatar_margin.resToPx(this)
val cardWidth = ViewGroup.LayoutParams.MATCH_PARENT
val cardHeight = R.dimen.profile_card_height.resToPx(this).toInt()
//2
val painter = ProfileCardPainter(
  color = azureColor
)
//3
profileCardContainer.addView(
  CustomPainter(
    context = this,
    width = cardWidth,
    height = cardHeight,
    painter = painter
  )
)

Add any missing import by pressing Option+Enter on Mac or Alt+Enter on PC.

In the code above:

  • context to create this custom Android View.
  • width and height of the custom shape.
  • painter responsible for all the drawing logic.
  1. You define the properties of your custom shape: Color, avatar radius, avatar margin, width and height.
  2. Then, you create a ProfileCardPainter with the color you previously defined.
  3. Finally, you add a new CustomPainter as a subview of profileCardContainer by passing all its needed properties:

Build and run the app to see… a pretty ugly card because you haven’t drawn anything yet. Don’t worry, you’ll start drawing something in a moment. :]

Initial changes to the app

Drawing Your First Shape

In this section, you’ll practice with the tools you need to draw in the computer graphics world. They’re a lot like the physical tools you used to draw a circle on a paper. Then, with this knowledge, you’ll draw your first shape!

Note: Graphics libraries have similar APIs for drawing, which makes drawing in Android comparable to drawing in iOS, Flutter and the web. When you master drawing custom shapes on one platform, it’s easy to reuse this knowledge on other platforms.

Drawing and Painting a Rectangle

To draw a rectangle, you need to create a RectF object with the size you want. You then need a Paint object with the color you prefer to start drawing that RectF on the canvas.

RectF is a simple class with four immutable float properties: Left, top, right and bottom. These four numbers represent a rectangle, where:

  • Left is the left-most point on the x-axis.
  • Top is the top-most point on the y-axis.
  • Right is the right-most point on the x-axis.
  • Bottom is the bottom-most point on the y-axis.

In this tutorial, you’ll rely on RectF for your shape bounds. You’ll draw each shape inside of and based on a certain RectF.

Note: You can calculate any extra properties in RectF, like the width and height, based on these four main properties.

In ProfileCardPainter.kt, go to paint() and add the following:

//1
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
//2
val shapeBounds = RectFFactory.fromLTWH(0f, 0f, width, height)
//3
val paint = Paint()
paint.color = color
//4
canvas.drawRect(shapeBounds, paint)

Add any missing import by pressing Option+Enter on Mac or Alt+Enter on PC.

Here’s what this code defines:

  1. The width and height of the canvas.
  2. shapeBounds is a RectF with a size that fits the whole area of the canvas by using the factory function fromLTWH().
  3. paint is your paint and its color.
  4. Finally, you draw your shapeBounds on the canvas by passing it to drawRect() along with your paint from the previous line.

Now, build and run the app. See that the card now has a blue rectangle as its background. Hooray, you’ve drawn your first shape! :]

The starter app showing a plain bio card

That’s better, but there’s still much room for improvement!

Using a Path to Draw the Profile Card

A path is not a bitmap or raster, and it doesn’t have pixels. It’s an outline that represents a series of smooth lines, arcs or Bézier curves. Using a path makes your shapes scalable and independent of the screen’s resolution.

Path is a powerful class that you can use in many situations. For example, you can clip a bitmap by a path, or you can use a path to draw a custom shape like you’re about to do right now.

Drawing the Profile Card

In this section, you’ll start using the Path class to draw a more complex shape like the blue shape here:

Profile card shape

But before you start, you need to do some preparation.

There are a few things you should note in the previous image:

  • Black dashed rectangle: Represents the whole canvas.
  • Red dashed rectangle: Marks the bounds of the blue shape. It has the same width and height as the canvas, except that you subtract the avatar radius from its height.
  • Blue shape: A rectangle with a half circle, an arc of a circle, as a negative space at the bottom center. This arc should have a radius equal to the radius of the avatar.
Note: An arc is a segment of a curve. In this case, the arc you’ll use is a section of a circle’s circumference, also called a circular arc.

The image below shows a blue arc that starts at the zero degree angle and sweeps to 90 degrees.

90 degrees arc

First, get the radius of the avatar. Start by adding a new class property called avatarRadius to your ProfileCardPainter primary constructor:

class ProfileCardPainter(
    @ColorInt private val color: Int,
    private val avatarRadius: Float
) : Painter {

Then, go to MainActivity.kt and, in onCreate(), pass the avatarRadius to ProfileCardPainter:

val painter = ProfileCardPainter(
  color = azureColor,
  avatarRadius = avatarRadius
)

Finally, return to ProfileCardPainter.kt and update the shapeBounds by subtracting the avatarRadius from its height in fromLTWH():

val shapeBounds = RectFFactory.fromLTWH(0f, 0f, width, height - avatarRadius)

The avatar radius

To see the results build and run the app:

Initial results for your custom painter

Great! Now the blue background stops halfway down the length of the avatar.