Understanding CustomPaint, CustomPainter, and the Canvas

James Williams
Jan 8 · 4 min read

In my last post, we talked about a generalized strategy for approaching the Flutter Clock Challenge. In this post, we’ll dig into drawing on the canvas using the CustomPaint widget.

CustomPaint and CustomPainter

For all the powerful things you can do with a CustomPaint, its API surface is relatively small. It allows you set a CustomPainter that serves as a background for the widget, an optional child that will draw itself above the painter, and an optional foreground painter to draw in front of the child widget.

CustomPaint( 
painter: <CustomPainter>,
child: <Widget>, /* optional */
foregroundPainter: <CustomPainter>, /* optional */
)

The real power comes from CustomPainter and its use of the canvas. CustomPainter has the following signatures.

class MyClass extends CustomPainter { 
@override void paint(Canvas size, Size size) { /*...*/ }
@override bool shouldRepaint(CustomPainter oldDelegate) { }
}

Canvas

The Canvas class exposes drawing commands for a number of operations including:

  • points, lines, arcs/ellipses, and polygonal shapes,
  • paths,
  • text,
  • shadows, and,
  • clipping.

Most of the operations take the form of draw* and may take an Offset, individual double s, and a Paint to draw the object. One thing to keep an eye on if coming a graphics package in another language is that Offset changes what it means depending on where it is used. Sometimes, it's an delta from another coordinate, other times it represents a position. From the Flutter docs,

Generally speaking, Offsets can be interpreted in two ways:

1. As representing a point in Cartesian space a specified distance from a separately-maintained origin. For example, the top-left position of children in the RenderBox protocol is typically represented as an Offset from the top left of the parent box.

2. As a vector that can be applied to coordinates. For example, when painting a RenderObject, the parent is passed an Offset from the screen’s origin which it can add to the offsets of its children to find the Offset from the screen’s origin to each of the children.

Here’s a painter that draws a target on a canvas using circles with alternating colors and decreasing radii. For the paints, I used the built-in Material Colors class. You can alternatively specify a color directly in hex using AARRGGBB format.

class TargetPainter extends CustomPainter {@override
void paint(Canvas canvas, Size size) {
// Offset sets each circle's center
canvas.drawCircle(
Offset(200,200), 200, Paint()..color = Colors.white);
canvas.drawCircle(
Offset(200,200), 150, Paint()..color = Colors.red);
canvas.drawCircle(
Offset(200,200), 100, Paint()..color = Colors.white);
canvas.drawCircle(
Offset(200,200), 50, Paint()..color = Color(0xFFFF0000);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
TargetPainter shown in DartPad

Here’s another painter creating a Sierpinksi carpet (shown in article hero image), a plane fractal formed by subdividing the plane into 9 parts, removing the center and then recursively subdividing those parts.

Sierpinski Carpet, a planar fractal

Transforming objects

Drawing a circle or rectangle where we can directly specify the constraints is fine but what happens when we can’t directly set a position, or need to rotate or scale. The short answer is that canvas provides answers to translate, scale, and rotate. The long answer is that the order of your transformations matter. They get boiled down into a transformation matrix(an evil math construct) and individual collections of transforms can be made affect all or parts of a drawing using save and restore.

In this snippet of a painter, to draw the leaves of the flower, we save the state of the transform matrix with canvas.save(), do our transforms and drawing and then restore the previous state when done.

The transform matrix works like a Stack. Each new draw instruction looks at the accumulated matrix from the stack, applies it and draws the object. When canvas.restore is called, the previous saved state is popped from the stack. In the case of the flower, the (0,300) translation is done before the first save. So it will affect all the subsequent drawing calls but any transforms enclosed in save/restore will not leak to others.

paint function of a flower painter
Dartpad running the flower painter code

Rendering Paths

The canvas can also render paths. The following graphic was derived from an Inkscape drawing. Flutter’s Path class provides many functions mirroring SVG paths to create complex paths. Given the right calculations, you can simulate 3D like in the following painter.

Dartpad running the 3D Box painter code

This was a relatively quick world wind tour of the capabilities of CustomPaint, CustomPainter, and the Canvas. The upside of Flutter’s canvas is that if you are familiar with OpenGL, HTML5 Canvas, or really any low level drawing API, you’ll be fine with it. My samples in this post were a mix of ports from an old Udacity course on libGDX (Java game library), adapting an SVG file, and a bespoke example.

The Dartpad holding all the code is here: https://dartpad.dev/b9553214b6cf88410bff2eb5cc89153c

James Williams

Written by

Android Certification SWE, Advanced Systems Group at Google, Fmr Android Lead @Udacity

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade