How to Paint in Flutter

A simple guide for learning to use the CustomPaint widget

Image for post
Image for post
Photo by Steve Johnson

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.)
  • 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.
  • The CustomPainter subclass overrides two methods: paint() and shouldRepaint().
  • You will do your custom painting in paint(). 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

Image for post
Image for post
import 'dart:ui' as ui;
@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);
}
  • 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

Image for post
Image for post
@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);
}

Rectangles

Image for post
Image for post
@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);
}

Circles

Image for post
Image for post
@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

Image for post
Image for post
@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);
}

Arcs

Image for post
Image for post
import 'dart:math' as math;
@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);
}
  • 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.
  • 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 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

Image for post
Image for post
@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);
}
  • 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

Image for post
Image for post

Low level version

Add the following import:

import 'dart:ui' as ui;
@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);
}
  • 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 a Paragraph, which Canvas uses to draw the text. You style the text by pushing and popping the TextStyle 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);
}
  • 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.
  • 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:

A Flutter and Dart developer. Follow me on Twitter @suragch1 to get updates of new articles.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store