How to Paint in Flutter
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 size300 x 300
logical pixels. (If you do give it a child widget, then CustomPaint will take its size. Thepainter
will paint below the child widget andforegroundPainter
will paint on top of the child widget.) - The
CustomPaint
widget takes aCustomPainter
object (note the “-er” ending) as a parameter. TheCustomPainter
gives you a canvas that you can paint on. - The
CustomPainter
subclass overrides two methods:paint()
andshouldRepaint()
. - You will do your custom painting in
paint()
. For all of my examples below, insert the code here. shouldRepaint()
is called when theCustomPainter
is rebuilt. If you returnfalse
then the framework will use the previous result of paint (thus saving work). But if you returntrue
thenpaint()
will get called again. You could do something like check ifoldPainter.someParameter != someParameter
to make the decision. But we don’t have changing parameters today so we will returnfalse
.
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);
}
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)
. - 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);
}
Notes:
- The
drawLine
method draws a line connecting the two points you give it. You could do the same thing withdrawPoints
using thePointMode.lines
orPointMode.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);
}
Notes:
- Using
PaintingStyle.stroke
made the rectangle outlined. If you wanted it filled you could usePaintingStyle.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);
}
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);
}
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);
}
Notes:
- The
rect
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 of0
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. - If you set
useCenter
totrue
, 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);
}
Notes:
moveTo
goes to where the path starts.lineTo
draws a line from the current location in the path to the given(x, y)
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. - 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);
}
Notes:
- When using low level methods from
dart:ui
it is customary to prefix the classes withui.
. This also helps with naming conflicts. For example,TextStyle
is also defined in painting library. If you used thatTextStyle
, then you would need to encode it fordart:ui
withTextStyle().getTextStyle()
for a single style orTextStyle().build()
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.- A
ParagraphBuilder
is used to build aParagraph
, whichCanvas
uses to draw the text. You style the text by pushing and popping theTextStyle
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);
}
Notes:
TextPainter
callscanvas.drawParagraph
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 isrtl
(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:
drawImage
drawImageNine
(nine patch)drawShadow
You can learn more in these resources: