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
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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) {
  //1
  final paint = Paint()..color = color;

  //2
  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

  //9
  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:

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.

  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.
  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():

//1
final centerAvatar = Offset(shapeBounds.center.dx, shapeBounds.bottom);
//2
final avatarBounds = Rect.fromCircle(center: centerAvatar, radius: avatarRadius);
//3
_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.

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.

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) {
  //1
  final paint = Paint()..color = color.darker();

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

  //3
  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

  //10
  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:

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.

  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.
  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:

//1
final curvedShapeBounds = Rect.fromLTRB(
  shapeBounds.left,
  shapeBounds.top + shapeBounds.height * 0.35,
  shapeBounds.right,
  shapeBounds.bottom,
);

//2
_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.