A simple guide for learning to use the CustomPaint widget

If you haven’t seen it already, you might want to start by watching the Flutter Widget of the Week video about CustomPaint. I’ll be showing how to do many of the things in that video.

Setup

Create a new project and replace main.dart with the following code:

Notes:

To paint in Flutter you use the CustomPaint widget. If you don’t give it a child, you should set the size. Here I made the size 300 x 300 logical pixels. (If you do give it a child widget, then CustomPaint will take its size. The painter will paint below the child widget and foregroundPainter will paint on top of the child widget.)

widget. If you don’t give it a child, you should set the size. Here I made the size logical pixels. (If you do give it a child widget, then CustomPaint will take its size. The will paint below the child widget and will paint on top of the child widget.) The CustomPaint widget takes a CustomPainter object (note the “-er” ending) as a parameter. The CustomPainter gives you a canvas that you can paint on.

widget takes a object (note the “-er” ending) as a parameter. The gives you a canvas that you can paint on. The CustomPainter subclass overrides two methods: paint() and shouldRepaint() .

subclass overrides two methods: and . You will do your custom painting in paint() . For all of my examples below, insert the code here.

. For all of my examples below, insert the code here. shouldRepaint() is called when the CustomPainter is rebuilt. If you return false then the framework will use the previous result of paint (thus saving work). But if you return true then paint() will get called again. You could do something like check if oldPainter.someParameter != someParameter to make the decision. But we don’t have changing parameters today so we will return false .

Points

Add the following import:

import 'dart:ui' as ui;

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final pointMode = ui.PointMode.points;

final points = [

Offset(50, 100),

Offset(150, 75),

Offset(250, 250),

Offset(130, 200),

Offset(270, 100),

];

final paint = Paint()

..color = Colors.black

..strokeWidth = 4

..strokeCap = StrokeCap.round;

canvas.drawPoints(pointMode, points, paint);

} void paint(Canvas canvas, Size size) {final pointMode = ui.PointMode.points;final points = [Offset(50, 100),Offset(150, 75),Offset(250, 250),Offset(130, 200),Offset(270, 100),];final paint = Paint()..color = Colors.black..strokeWidth = 4..strokeCap = StrokeCap.round;canvas.drawPoints(pointMode, points, paint);

Notes:

You should stay within the bounds of size .

. An Offset is a pair of (dx, dy) doubles, offset from the top left corner, which is (0, 0) .

is a pair of doubles, offset from the top left corner, which is . If you don’t set the color, the default is white.

Lines

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final p1 = Offset(50, 50);

final p2 = Offset(250, 150);

final paint = Paint()

..color = Colors.black

..strokeWidth = 4;

canvas.drawLine(p1, p2, paint);

} void paint(Canvas canvas, Size size) {final p1 = Offset(50, 50);final p2 = Offset(250, 150);final paint = Paint()..color = Colors.black..strokeWidth = 4;canvas.drawLine(p1, p2, paint);

Notes:

The drawLine method draws a line connecting the two points you give it. You could do the same thing with drawPoints using the PointMode.lines or PointMode.polygon options.

Rectangles

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final left = 50.0;

final top = 100.0;

final right = 250.0;

final bottom = 200.0;

final rect = Rect.fromLTRB(left, top, right, bottom);

final paint = Paint()

..color = Colors.black

..style = PaintingStyle.stroke

..strokeWidth = 4;

canvas.drawRect(rect, paint);

} void paint(Canvas canvas, Size size) {final left = 50.0;final top = 100.0;final right = 250.0;final bottom = 200.0;final rect = Rect.fromLTRB(left, top, right, bottom);final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 4;canvas.drawRect(rect, paint);

Notes:

Using PaintingStyle.stroke made the rectangle outlined. If you wanted it filled you could use PaintingStyle.fill .

Circles

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final center = Offset(150, 150);

final radius = 100.0;

final paint = Paint()

..color = Colors.black

..style = PaintingStyle.stroke

..strokeWidth = 4;

canvas.drawCircle(center, radius, paint);

} void paint(Canvas canvas, Size size) {final center = Offset(150, 150);final radius = 100.0;final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 4;canvas.drawCircle(center, radius, paint);

Ovals

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final rect = Rect.fromLTRB(50, 100, 250, 200);

final paint = Paint()

..color = Colors.black

..style = PaintingStyle.stroke

..strokeWidth = 4;

canvas.drawOval(rect, paint);

} void paint(Canvas canvas, Size size) {final rect = Rect.fromLTRB(50, 100, 250, 200);final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 4;canvas.drawOval(rect, paint);

Notes:

LTRB stands for left, top, right, bottom.

Arcs

Add the following import:

import 'dart:math' as math;

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final rect = Rect.fromLTRB(50, 100, 250, 200);

final startAngle = -math.pi / 2;

final sweepAngle = math.pi;

final useCenter = false;

final paint = Paint()

..color = Colors.black

..style = PaintingStyle.stroke

..strokeWidth = 4;

canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);

} void paint(Canvas canvas, Size size) {final rect = Rect.fromLTRB(50, 100, 250, 200);final startAngle = -math.pi / 2;final sweepAngle = math.pi;final useCenter = false;final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 4;canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);

Notes:

The rect is what the full oval would be inscribed within.

is what the full oval would be inscribed within. The startAngle is the location on the oval that the line starts drawing from. An angle of 0 is at the right side. Angles are in radians, not degrees. The top is at 3π/2 (or -π/2), the left at π, and the bottom at π/2.

is the location on the oval that the line starts drawing from. An angle of is at the right side. Angles are in radians, not degrees. The top is at 3π/2 (or -π/2), the left at π, and the bottom at π/2. The sweepAngle is how much of the oval is included in the arc. Again, angles are in radians. A value of 2π would draw the entire oval.

is how much of the oval is included in the arc. Again, angles are in radians. A value of 2π would draw the entire oval. If you set useCenter to true , then there will be a straight line from both sides of the arc to the center. It will look like a piece of pie.

Paths

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final path = Path()

..moveTo(50, 50)

..lineTo(200, 200)

..quadraticBezierTo(200, 0, 150, 100);

final paint = Paint()

..color = Colors.black

..style = PaintingStyle.stroke

..strokeWidth = 4;

canvas.drawPath(path, paint);

} void paint(Canvas canvas, Size size) {final path = Path()..moveTo(50, 50)..lineTo(200, 200)..quadraticBezierTo(200, 0, 150, 100);final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 4;canvas.drawPath(path, paint);

Notes:

moveTo goes to where the path starts.

goes to where the path starts. lineTo draws a line from the current location in the path to the given (x, y) coordinates.

draws a line from the current location in the path to the given coordinates. The first two arguments of the quadraticBezierTo method are the x,y values of the control point. The last two arguments are the x,y values of where the Bezier curve ends.

method are the x,y values of the control point. The last two arguments are the x,y values of where the Bezier curve ends. There are a lot more options for making paths, so that will have to wait for another lesson.

Text

Low level version

Add the following import:

import 'dart:ui' as ui;

Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final textStyle = ui.TextStyle(

color: Colors.black,

fontSize: 30,

);

final paragraphStyle = ui.ParagraphStyle(

textDirection: TextDirection.ltr,

);

final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)

..pushStyle(textStyle)

..addText('Hello, world.');

final constraints = ui.ParagraphConstraints(width: 300);

final paragraph = paragraphBuilder.build();

paragraph.layout(constraints);

final offset = Offset(50, 100);

canvas.drawParagraph(paragraph, offset);

} void paint(Canvas canvas, Size size) {final textStyle = ui.TextStyle(color: Colors.black,fontSize: 30,);final paragraphStyle = ui.ParagraphStyle(textDirection: TextDirection.ltr,);final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)..pushStyle(textStyle)..addText('Hello, world.');final constraints = ui.ParagraphConstraints(width: 300);final paragraph = paragraphBuilder.build();paragraph.layout(constraints);final offset = Offset(50, 100);canvas.drawParagraph(paragraph, offset);

Notes:

When using low level methods from dart:ui it is customary to prefix the classes with ui. . This also helps with naming conflicts. For example, TextStyle is also defined in painting library. If you used that TextStyle , then you would need to encode it for dart:ui with TextStyle().getTextStyle() for a single style or TextStyle().build() to apply the style tree recursively.

it is customary to prefix the classes with . This also helps with naming conflicts. For example, is also defined in painting library. If you used that , then you would need to encode it for with for a single style or to apply the style tree recursively. If your project has a white background be sure to set the text style color to black or something you can see. The default text color is white.

ltr means left-to-right.

means left-to-right. A ParagraphBuilder is used to build a Paragraph , which Canvas uses to draw the text. You style the text by pushing and popping the TextStyle as you add text strings.

is used to build a , which uses to draw the text. You style the text by pushing and popping the as you add text strings. Before you can paint the text, you have to lay it out. This task is passed down to the Skia engine.

High(er) lever version

No need to import dart:ui . Replace MyPainter.paint() with the following code:

@override

void paint(Canvas canvas, Size size) {

final textStyle = TextStyle(

color: Colors.black,

fontSize: 30,

);

final textSpan = TextSpan(

text: 'Hello, world.',

style: textStyle,

);

final textPainter = TextPainter(

text: textSpan,

textDirection: TextDirection.ltr,

);

textPainter.layout(

minWidth: 0,

maxWidth: size.width,

);

final offset = Offset(50, 100);

textPainter.paint(canvas, offset);

} void paint(Canvas canvas, Size size) {final textStyle = TextStyle(color: Colors.black,fontSize: 30,);final textSpan = TextSpan(text: 'Hello, world.',style: textStyle,);final textPainter = TextPainter(text: textSpan,textDirection: TextDirection.ltr,);textPainter.layout(minWidth: 0,maxWidth: size.width,);final offset = Offset(50, 100);textPainter.paint(canvas, offset);

Notes:

TextPainter calls canvas.drawParagraph internally (like we did above).

calls internally (like we did above). Flutter makes an effort to not assume a text direction, so you need to set it explicitly. The abbreviation ltr stands for left-to-right, which languages like English use. The other option is rtl (right-to-left), which languages like Arabic and Hebrew use. This helps to reduce bugs when the code is used in language contexts that developers were not thinking about.

stands for left-to-right, which languages like English use. The other option is (right-to-left), which languages like Arabic and Hebrew use. This helps to reduce bugs when the code is used in language contexts that developers were not thinking about. Even with this higher level version, you still have to layout the text before you paint it.

Going on

There are more things you can draw than I covered here. Here are a few more to check out:

You can learn more in these resources: