Drawing Custom Shapes With CustomPainter in Flutter

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

Leave a rating/review
Download materials
Save for later

Did you ever wish you could be a painter? Or maybe you just want to create highly-customized user interfaces for your apps? Either way, Flutter lets you make those wishes come true.

To draw custom shapes, you need to keep iterating until you achieve the beautiful art you want. That can be painful in native iOS and Android development, because every time you make a change, even something small like changing a color, you’ll need to build and wait for some time.

With CustomPainter, Flutter gives you access to low-level graphics painting. Best of all, painting in Flutter is fast and efficient. For example, Flutter’s hot reload feature makes it easy to iterate quickly until you get exactly the look you want

In this tutorial, you’ll dip your hands in paint by improving an app called Stars of Science. You’ll learn how to create custom shapes by painting a profile card with a curved custom shape and gradient colors.

Throughout the tutorial, you’ll learn how to:

  • Prepare a custom shape on paper before coding.
  • Use CustomPainter and many Flutter painting APIs.
  • Draw a curved shape in gradient colors.

The custom shape you’ll create will look like this:

flutter custompainter

Note: This tutorial assumes prior knowledge of Dart and the Flutter framework for developing cross-platform mobile apps. If you’re unfamiliar with Flutter, please see Getting Started with Flutter.

Getting Started

Download the starter project using the Download Materials button at the top or bottom of the tutorial and unzip it. You’ll find the files you need to start working on your app, along with some widgets.

Your app already has its basic UI set up so you can focus on drawing custom shapes in Flutter.

Start Android Studio 3.5 or later with the Flutter plugin installed, then choose the Open an existing Android Studio project option. Select the starter project folder from your unzipped download.

After opening the project, click Get dependencies on the Packages get message near the top of Android Studio to pull down the project dependencies.

Before you make any changes to the starter project, press the green Run button in Android Studio to run the app. You’ll see the following screen on your mobile phone, iOS Simulator or Android emulator:

The starter app showing a plain bio card

It’s not bad, but the top of the card doesn’t have much pizazz. You’ll change that over the course of the tutorial.

Exploring the Project

Take a quick look at the project structure. Expand lib and check out the folders inside:

Project structure

The profile folder contains three files. The most important for this tutorial is the profile_card.dart file, which is where you’ll start coding.

  • profile_page.dart: Includes the profile card and a short bio of the scientist.
  • profile_card.dart: Displays the name, title, and avatar of the scientist.
  • profile_model.dart: A data model with mocked data.

The lib folder also contains three files. Here’s a short description of each:

  • main.dart: The main() method and the MaterialApp widget.
  • widgets.dart: A few small widgets to display some details.
  • extensions.dart: A Dart extension method on the Color class, which gives you a darker shade of any color.

Now that you know more about the files you’ll work with, take a moment to learn some of the theory behind making beautiful shapes.

Coding Your Shapes

Before diving into drawing with Flutter CustomPainter, you need to know which tools you’ll need, how to use them and how to prepare to code your target shape.

Think about drawing in the physical world. To draw a shape, you need to get a pencil and paper, then you need to use your hand to move the pencil across the paper’s surface to draw a shape. Finally, if you want to make it beautiful, you need to get a coloring brush with some colors.

In this section, you’ll start by drawing a shape freehand. Grab a pencil and paper and get ready!

Know Your Canvas

Your canvas acts as the digital version of the piece of paper you draw on. It holds all your drawing elements including lines, curves, arches, shapes, text, images and so on.

The canvas needs to have a size including a width and a height. Drawing on a canvas without knowing its size can lead to unexpected results.

Very important: You don’t want your shapes to have an absolute position or size. Instead, you want to make them responsive to the canvas’ size. This allows you to display your shapes on different devices with different screen sizes.

For instance, you might set your shape at the center of the canvas or make its size equal to half of the canvas size.

On your paper, before drawing any shape, define the canvas by drawing a rectangle of any size you want. Any shapes you draw later will be relative to that canvas.

Rectangle on paper

Now that you have a canvas, you want to be able to create a shape inside of it.

Defining How to Move Your Brush

In visual arts, you need to move your brush properly across the paper’s surface to create your art. You’ll use the same mechanism to draw on the canvas.

Before you can draw a shape, you need to consider the functionalities the canvas object needs to have.

For instance, if you want to draw a square, you need to draw four lines — so you need to use the drawing line function in your framework. On the other hand, if you want to draw a crescent, you need to draw two curves — so the drawing curve function in your framework is the tool you need.

Hold your pencil again and draw a circle that fits in a quarter of the canvas’ width at the center of the canvas, like this:

Draw a circle on paper

Now, to convert that shape on your paper into a shape in Flutter, you need to consider its coordinates.

Calculating Coordinates

Coordinates are pairs of numbers that define the exact location of a point on a plane.

Before you can draw anything, you need to know the main points that make up that shape. For good practice, calculate all the coordinates on your paper before writing any code. This saves you coding time and it lets you focus on translating that shape from the paper onto your device.

Since you already drew a circle relative to the canvas on your paper, you already calculated two things:

  1. The center of the circle:
    Since your circle is at the center of the canvas, the center of the circle is the center of the canvas. So the x coordinate of the circle’s center is equal to half of the width of the canvas and the y coordinate of the circle’s center is equal to half of the height of the canvas. This means that:
    cx = canvas width / 2
    cy = canvas height / 2
  2. The radius:
    Since your circle is a quarter of the canvas width, the diameter of the circle is equal to a quarter of the width of the canvas. As you know, the radius is equal to the half of the diameter. That means that:
    diameter = canvas width / 4
    radius = diameter / 2 = canvas width / 8

Circle properties

Now, you’ve seen that drawing your shapes on paper helps you calculate the points you need to draw your shape relative to the canvas.

This is an efficient way to understand exactly what you need to do when it’s time to translate your ideas into code. Always make paper sketches a prerequisite for your custom drawing! :]

Using CustomPainter

Now that you’ve learned some theory, it’s time to start using CustomPainter.

Implementing the CustomPainter Class

Start by creating a new Dart file named profile_card_painter.dart in lib/profile. Then add the following code:

import 'package:flutter/material.dart';
class ProfileCardPainter extends CustomPainter {
  ProfileCardPainter({@required this.color});

  final Color color;

  void paint(Canvas canvas, Size size) {}

  bool shouldRepaint(ProfileCardPainter oldDelegate) {
    return color != oldDelegate.color;

Here’s what this code does:

  1. You create a new class named ProfileCardPainter and extend the abstract class CustomPainter.
  2. Then, you create a constructor to pass the profile color as a named and required parameter.
  3. You create a final class property for the profile color.
  4. Then you implement paint(Canvas canvas, Size size). Flutter will call this method whenever the object needs to paint.

    You’ll write all of your drawing code inside this method, which gives you two parameters:
    – The canvas to draw on.
    – The size of the canvas. You’ll draw your shapes inside of or relative to its bounds.

  5. Finally, you implement shouldRepaint(CustomPainter oldDelegate). Flutter calls this method whenever it needs to re-render CustomPainter. It gives you one parameter, which is the old instance of CustomPainter.

    Ideally, you’d compare the old instance properties to the current ones and, if they’re equivalent, return false to not repaint. Otherwise, return true to repaint. So here, you compare the current color to the color of the oldDelegate.

There is still one minor detail: ProfileCardPainter isn’t a widget, but you need a widget to provide a canvas to render the paint. That’s where CustomPaint comes in.

Rendering With CustomPaint

CustomPaint supports two optional painter parameters: painter and foregroundPainter. The first paints before the child widget, while the second paints after the child.

Go to profile/profile_card.dart and find ProfileCard‘s build(). Replace the first child container in Stack with a CustomPaint widget with a ProfileCardPainter. You also need to import profile_card_painter.dart. The code will look like this:

return Stack(
      children: <Widget>[
          size: Size.infinite, //2
          painter: ProfileCardPainter(color: profileColor), //3

Here, you:

  1. Add a CustomPaint to render your custom shapes.
  2. Set the size to Size.infinite to let the CustomPaint widget fit its parent.
  3. Create a new ProfileCardPainter and pass the profileColor to its constructor.

Hot restart the app and you’ll see… a pretty ugly card because you haven’t drawn anything yet. Don’t worry you’ll start drawing next. :]

Initial changes to the app

Drawing Your First Shape

In this section, you’ll get to know the tools you need to draw in the computer graphics world by drawing your first shape. It’s a lot like the physical tools you used to draw a circle on a paper.

Fortunately, most graphics libraries have similar APIs for drawing, which makes drawing in Flutter common to drawing on Android, iOS, and the web.

Drawing and Painting a Rectangle

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

What Is Rect?

Rect is a simple class with four immutable double properties: left, top, right and bottom. These four numbers represent a rectangle, where:

  • left: The left-most point on the x-axis.
  • top: The top-most point on the y-axis.
  • right: The right-most point on the x-axis.
  • bottom: The bottom-most point on the y-axis.
Note: You can calculate any extra properties in Rect, like the width, height and so on, based on these four main properties.
Note: In this tutorial, you’ll rely on Rect for your shape bounds. You’ll draw each shape inside of and based on a certain Rect.

In profile_card_painter.dart, go to ProfileCardPainter‘s paint() method and add the following:

final shapeBounds = Rect.fromLTRB(0, 0, size.width, size.height);
final paint = Paint()..color = color;
canvas.drawRect(shapeBounds, paint);

Here, you:

  1. Create a Rect with a size that fits the whole area of the canvas by using the named constructor fromLTRB().
  2. Create a Paint and set its color.
  3. Draw the Rect on the canvas by passing it to drawRect() along with Paint from the previous line.

Hooray, you’ve drawn your first shape! :]

Hot reload the app to see that the card now has a blue rectangle for the background:

Your first shape

That’s better, but there’s still a lot of 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 widget by a path using ClipPath, you can set a custom border to a widget by implementing ShapeBorder 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:

  • The black dashed rectangle represents the whole canvas.
  • The red dashed rectangle marks the bounds of the blue shape, which has the same width and height as the canvas, except that you subtract the avatarRadius from its height.
  • The blue shape is a kind of rectangle, but it has 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 starting from the zero angle and doing a 90° sweep.

90 degree arc

Your first step is to get the radius of the avatar. Start by adding a new class property called avatarRadius to your ProfileCardPainter class, then initialize it in the constructor.

ProfileCardPainter({@required this.color, @required this.avatarRadius});

final Color color;
final double avatarRadius;

Then, go to profile/profile_card.dart and, in ProfileCard‘s build(), pass the avatarRadius to the ProfileCardPainter constructor:

  size: Size.infinite,
  painter: ProfileCardPainter(color: profileColor, avatarRadius: avatarRadius),

Finally, return to ProfileCardPainter and update the shapeBounds by subtracting the avatarRadius from its height.

The avatar radius

final shapeBounds = Rect.fromLTWH(0, 0, size.width, size.height - avatarRadius);

Hot reload the app to see the results:

Initial results for your custom painter

Great, now the blue background stops halfway down the length of the avatar.

Adding Negative Space Around the Avatar

For your next step, you’ll add some negative space to the blue shape to set it apart from the avatar. To start, add a new method called _drawBackground() to your ProfileCardPainter class:

void _drawBackground(Canvas canvas, Rect shapeBounds, Rect avatarBounds) {
  final paint = Paint()..color = color;

  final backgroundPath = Path()
    ..moveTo(shapeBounds.left, shapeBounds.top) //3
    ..lineTo(shapeBounds.bottomLeft.dx, shapeBounds.bottomLeft.dy) //4
    ..arcTo(avatarBounds, -pi, pi, false) //5
    ..lineTo(shapeBounds.bottomRight.dx, shapeBounds.bottomRight.dy) //6
    ..lineTo(shapeBounds.topRight.dx, shapeBounds.topRight.dy) //7
    ..close(); //8

  canvas.drawPath(backgroundPath, paint);

Then, import the Dart Math library so you can access the pi constant:

import 'dart:math';

To understand this new code, use the following image as a guide to know what the proper coordinates are for each point you need to build the path.

Custom path

In the previous code, you:

  1. Create a Paint object and set its color.
  2. Create a Path object.
  3. Move to the top-left corner — P1 — without drawing a line. This is like moving a brush to a starting point without touching the paper.
  4. Add a straight line that starts from P1 and ends at P2.
  5. Start from the current point — P2 — and draw a straight line to the edge of the Rect. Then add an arc at the segment of the Rect.

    The arc starts from the angle -pi radians and sweeps by pi radians. Finally, you pass false as the last parameter so you don’t start a new sub-path for the arc. This tells Flutter that you want to let the arc be on the same path.

  6. Next, add a straight line that starts from the current point and ends at the given point, which is P5 at the bottom-right. This adds a line from P4 to P5.
  7. Finish by adding a straight line that starts from the current point, P5, and ends at the given point, which is P6 at the top-right.
  8. Close the path by adding a straight line that starts at the current point, P6, and ends at the beginning point on the path, P1.
  9. Draw the backgroundPath on the canvas by passing it to drawPath() with paint.

Next, go to paint() and replace the last two lines:

    final paint = Paint()..color = color; //remove
    canvas.drawRect(shapeBounds, paint); //remove

with the following code to create a new Rect that contains the avatar. Then, call _drawBackground():

final centerAvatar = Offset(shapeBounds.center.dx, shapeBounds.bottom);
final avatarBounds = Rect.fromCircle(center: centerAvatar, radius: avatarRadius);
_drawBackground(canvas, shapeBounds, avatarBounds);

Here, you:

  1. Create an Offset object for the center point of the avatar, where dx is the center.dx of the shapeBounds and dy is the bottom of the shapeBounds.
  2. Create a Rect object from the avatar circle using fromCircle(). The center is centerAvatar, which you just created and the radius is the avatarRadius.
  3. Call _drawBackground() and pass the canvas with rest of the parameters to draw your first path.

Finally, hot reload the app to see the following:

Custom path in the app

You don’t notice any difference! But don’t worry, you’ll fix that next.

Adding a Margin Around the Avatar

Actually, there is a difference, but you can’t see it because the negative space is exactly equal to the circular avatar’s size. Next, you’ll make that negative space a bit bigger to leave a margin between it and the avatar.

Go to the line where you create the avatarBounds and add .inflate(6) to the end:

final avatarBounds = Rect.fromCircle(center: centerAvatar,
  radius: avatarRadius).inflate(6);

Calling inflate() on a Rect creates a new Rect object whose left, top, right and bottom edges are moved outwards by the given value. The result is a nice space around the avatar.

Hot reload the app to see the margin.

Margin around the avatar

Pretty… but ordinary. Next, you’ll spice up the background by adding an interesting curved shape.

Adding More Neat Shapes

To enhance your custom shape, you can add some simple decorations like stars or circles in a partially-faded color. For this app, you’ll add a more interesting shape: a curvy shape in gradient colors.

Adding a Curved Shape

Before you start drawing, you need to know that there are different types of curves. Two that you should know are the Quadratic Bézier Curve and the Cubic Bézier Curve.

  • A quadratic Bézier curve is a curve that requires three points to draw: a start point, an end point and a handle point that pulls the curve towards it.
    Quadratic Bézier Curve
  • A cubic Bézier curve is a curve that needs four points to draw: a start point, an end point, and two handle points that pull the curve towards them.

    Cubic Bézier Curve

Your next step is to use a quadratic Bézier curve to create an interesting background shape.

Drawing a Quadratic Bézier Curve

Start by importing extensions.dart in profile_card_painter.dart. This lets you access the darker() extension method in ColorShades to get a darker shade of any color.

import '../extensions.dart';

Then, create a new method called _drawCurvedShape() inside ProfileCardPainter with the following code:

void _drawCurvedShape(Canvas canvas, Rect bounds, Rect avatarBounds) {
  final paint = Paint()..color = color.darker();

  final handlePoint = Offset(bounds.left + (bounds.width * 0.25), bounds.top);

  final curvePath = Path()
    ..moveTo(bounds.bottomLeft.dx, bounds.bottomLeft.dy) //4
    ..arcTo(avatarBounds, -pi, pi, false) //5
    ..lineTo(bounds.bottomRight.dx, bounds.bottomRight.dy) //6
    ..lineTo(bounds.topRight.dx, bounds.topRight.dy) //7
    ..quadraticBezierTo(handlePoint.dx, handlePoint.dy,
        bounds.bottomLeft.dx, bounds.bottomLeft.dy) //8
    ..close(); //9

  canvas.drawPath(curvePath, paint);

To understand this code, read the following instructions while using the image as a guide to the proper coordinates for each point you’ll build to create the path:

Path for new arc

In the previous code, you:

  1. Create a Paint object and set its color to a darker shade of the profile color.
  2. Create a handle point at the top left corner of the Rect, shifted to the right by 25% of the width of the Rect. This is P6 in the guide image.
  3. Create an Path object.
  4. Move to the bottom-left corner — P1 in the guide image.
  5. Add a straight line that starts from the current point,P1, to the edge of the Rect, the black, dashed square in the guide image.

    Then you add an arc at the segment of the Rect. The arc starts from the angle -pi radians and sweeps by pi radians. Finally, you pass false as the last parameter so you don’t start a new sub-path for the arc. This tells Flutter that you want to let the arc be on the same path.

  6. Add a straight line that starts from the current point and ends at the given point, the bottom-right corner. This adds a line from P3 to P4.
  7. Add a straight line that starts from the current point and ends at the given point, the top-right corner, adding a line from P4 to P5.
  8. Add quadratic Bézier curve that starts from the current point and ends at the bottom-left corner using the handle point you created in step 2.
  9. Close the path, though it’s not required this time since you are back at the beginning point on the path.
  10. Draw curvePath on the canvas by passing it to drawPath() along with the paint object.

Next, go to the last line in paint() and add the following code:

final curvedShapeBounds = Rect.fromLTRB(
  shapeBounds.top + shapeBounds.height * 0.35,

_drawCurvedShape(canvas, curvedShapeBounds, avatarBounds);

Here, you:

  1. Create a Rect that is similar to the shapeBounds rect, except that you’ve shifted its top slightly to the bottom by 35% of the shapeBounds‘ height.
  2. Call _drawCurvedShape() and pass the canvas object, the curved shape bounds and the avatar bounds to it.

Finally, hot reload the app to see the neat background curve behind the avatar:

Add a curve behind the avatar

So you’re done, right? Well, not quite. There’s one more finishing touch you still need to add.

Gradient Paint

You’ve created your first beautiful, custom curved shape, but your graphic designer wants you to do one more thing: add gradient colors to your curved shape.

There are different types of gradients including linear gradients, which transition through at least two colors in a straight line, and radial gradients, which transition through colors starting from a central point and radiating outward.

Right now, you’ll create a linear gradient described by three colors. Each color needs a stop to specify its position on a line from 0.0 to 1.0.

To start, go to the first line in _drawCurvedShape() and add the following code:

final colors = [color.darker(), color, color.darker()];
final stops = [0.0, 0.3, 1.0];
final gradient = LinearGradient(colors: colors, stops: stops);

Here’s what’s going on in this code:

  1. You create a list of three colors, where the middle color is the profile color and the first and last colors are darker shades of that profile color.
  2. Then you create a list of three stops. The first is 0.0, which puts the corresponding color in the colors list at the zero position of the gradient color. The middle and the last stop specify the positions of their corresponding colors.
  3. Finally, you create a linear gradient with the given colors and stops.

Next, update the paint object in the same method to use the new linear gradient instead of a solid color:

final paint = Paint()..shader = gradient.createShader(bounds);

Here, you create a shader from the gradient object. It will fill the given bounds. Then you set the shader to be the paint object.

Finally, hot reload the app to see a gradient within the background curve:

Background curve with gradient

Congratulations! You’ve created a beautiful profile card with an eye-catching custom background shape and shading.

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of the tutorial.

Wow, that was a lot of work! But you learned a lot, including:

  • How to prepare your custom shape on paper before coding.
  • A deep look at CustomPainter and many Flutter Painting APIs.
  • How to use Path and how to add different lines to it sequentially.
  • How to draw a curved shape in gradient colors.

To learn more about CustomPainter check out the following YouTube videos:

You can also visit this Medium article to learn how to draw custom decorations:

Feel free to share your feedback or ask any questions in the comments below or in the forums. Thank you!