Introduction to CustomPainter
Introduction to CustomPainter
Flutter's built-in widgets cover an enormous range of UI patterns, but sometimes you need to draw something that no widget provides — a custom chart, a unique progress indicator, a decorative shape, or a game sprite. For those cases, Flutter exposes a low-level Canvas API through the CustomPainter class. By subclassing CustomPainter and attaching it to a CustomPaint widget, you get direct access to the rendering surface and can draw anything you can describe mathematically.
The CustomPainter Contract
Every custom painter must implement exactly two methods:
paint(Canvas canvas, Size size)— called by the framework whenever the widget needs to be drawn. You issue drawing commands against theCanvasobject;Sizetells you how large the available area is.shouldRepaint(CustomPainter oldDelegate)— called before each potential repaint. Returntrueif the new painter's data is different from the old one and a redraw is needed; returnfalseto skip the expensive repaint.
shouldRepaint is a performance guard. Returning true unconditionally is safe but wasteful — every parent rebuild will trigger a full canvas redraw. Always compare the relevant fields and return false when nothing changed.Your First CustomPainter
The simplest possible painter subclasses CustomPainter, overrides the two required methods, and draws a filled circle at the centre of the canvas.
A Minimal CustomPainter
import 'package:flutter/material.dart';
class CirclePainter extends CustomPainter {
final Color color;
final double radius;
const CirclePainter({required this.color, required this.radius});
@override
void paint(Canvas canvas, Size size) {
// Define how the shape will be filled
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
// Draw a circle at the centre of the available area
final centre = Offset(size.width / 2, size.height / 2);
canvas.drawCircle(centre, radius, paint);
}
@override
bool shouldRepaint(CirclePainter oldDelegate) {
// Only repaint if the appearance actually changed
return oldDelegate.color != color || oldDelegate.radius != radius;
}
}
Attaching the Painter with CustomPaint
A CustomPainter instance is never placed in the widget tree directly. You wrap it in a CustomPaint widget, which allocates a canvas for the painter and manages its lifecycle.
Using CustomPaint in a Widget Tree
class PainterDemo extends StatelessWidget {
const PainterDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Custom Painter Demo')),
body: Center(
child: CustomPaint(
// Give the canvas an explicit size
size: const Size(300, 300),
painter: CirclePainter(
color: Colors.deepPurple,
radius: 100,
),
// child widgets render ON TOP of the painted content
child: const Center(
child: Text(
'Hello Canvas!',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
);
}
}
CustomPaint accepts both a painter (drawn behind the child) and a foregroundPainter (drawn in front of the child). Use foregroundPainter when you want the painted content to overlay the child widget, such as a badge or highlight ring.The Paint Object — Your Drawing Brush
Before issuing any canvas command, you configure a Paint object that acts like a brush. The most commonly used properties are:
color— the fill or stroke colour.style—PaintingStyle.fillfills the shape;PaintingStyle.strokedraws only the outline.strokeWidth— thickness of the stroke, in logical pixels.isAntiAlias— smooth curved edges (defaulttrue).shader— apply gradients or image patterns instead of a flat colour.
Key Canvas Drawing Commands
The Canvas class exposes a rich API. The most essential methods for beginners are:
canvas.drawCircle(Offset centre, double radius, Paint paint)canvas.drawRect(Rect rect, Paint paint)canvas.drawLine(Offset p1, Offset p2, Paint paint)canvas.drawPath(Path path, Paint paint)— for arbitrary shapes.canvas.drawOval(Rect rect, Paint paint)canvas.drawRRect(RRect rrect, Paint paint)— rounded rectangles.
CustomPaint widget, with x increasing to the right and y increasing downward. If your drawing appears clipped or off-screen, double-check that you are using the correct origin and that your shape fits within the provided Size.Triggering Repaints with AnimationController or setState
A static painter is useful, but the real power emerges when you combine CustomPainter with AnimationController or with a StatefulWidget that passes new data into the painter's constructor. Each time the parent calls setState with a new value, Flutter checks shouldRepaint and, if it returns true, calls paint again with the updated data — producing smooth, reactive graphics.
Summary
To draw custom graphics in Flutter you subclass CustomPainter, implement paint() to issue canvas commands, and implement shouldRepaint() to control when redraws happen. The painter is attached to the widget tree via the CustomPaint widget, which provides a correctly-sized canvas. The Paint object acts as your configurable brush, and the Canvas API exposes a rich set of drawing primitives. This foundation underpins everything from simple decorations to fully custom interactive graphics in Flutter.