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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Drawing Custom Shapes With CustomPainter in Flutter
30 mins
- Getting Started
- Exploring the Project
- Coding Your Shapes
- Know Your Canvas
- Defining How to Move Your Brush
- Calculating Coordinates
- Using CustomPainter
- Implementing the CustomPainter Class
- Rendering With CustomPaint
- Drawing Your First Shape
- Drawing and Painting a Rectangle
- Using a Path to Draw the Profile Card
- Drawing the Profile Card
- Adding Negative Space Around the Avatar
- Adding a Margin Around the Avatar
- Adding More Neat Shapes
- Adding a Curved Shape
- Drawing a Quadratic Bézier Curve
- Gradient Paint
- Where to Go From Here?
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';
//1
class ProfileCardPainter extends CustomPainter {
//2
ProfileCardPainter({@required this.color});
//3
final Color color;
//4
@override
void paint(Canvas canvas, Size size) {}
//5
@override
bool shouldRepaint(ProfileCardPainter oldDelegate) {
return color != oldDelegate.color;
}
}
Here’s what this code does:
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.
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.
- You create a new class named
ProfileCardPainter and extend the abstract class CustomPainter
. - Then, you create a constructor to pass the profile color as a named and required parameter.
- You create a final class property for the profile color.
- 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. -
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, returntrue
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>[
//1
CustomPaint(
size: Size.infinite, //2
painter: ProfileCardPainter(color: profileColor), //3
),
Here, you:
- Add a
CustomPaint
to render your custom shapes. - Set the size to
Size.infinite
to let theCustomPaint
widget fit its parent. - Create a new
ProfileCardPainter
and pass theprofileColor
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. :]
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.
Rect
, like the width, height and so on, based on these four main properties.
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:
//1
final shapeBounds = Rect.fromLTRB(0, 0, size.width, size.height);
//2
final paint = Paint()..color = color;
//3
canvas.drawRect(shapeBounds, paint);
Here, you:
- Create a
Rect
with a size that fits the whole area of the canvas by using the named constructorfromLTRB()
. - Create a
Paint
and set its color. - Draw the
Rect
on the canvas by passing it todrawRect()
along withPaint
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:
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:
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.
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:
CustomPaint(
size: Size.infinite,
painter: ProfileCardPainter(color: profileColor, avatarRadius: avatarRadius),
)
Finally, return to ProfileCardPainter and update the shapeBounds by subtracting the avatarRadius from its height.
final shapeBounds = Rect.fromLTWH(0, 0, size.width, size.height - avatarRadius);
Hot reload the app to see the results:
Great, now the blue background stops halfway down the length of the avatar.