When no widget fits the design, drawing on the canvas gives full control with excellent performance.
`CustomPaint` hands you a `Canvas` to draw on through a `CustomPainter`:
paint(Canvas canvas, Size size)draws usingcanvas.drawLine,drawRect,drawRRect,drawCircle,drawArc,drawPath,drawImage, and text viaTextPainter. Coordinates start at the top-left of the given size.- `Paint` objects describe style:
color,strokeWidth,style(fill or stroke),strokeCap,shaderfor gradients,maskFilterfor blur. - `Path` builds complex shapes with
moveTo,lineTo,quadraticBezierTo,cubicTo,arcToandaddOval, combined withPath.combine. canvas.save()/restore()withtranslate,rotate,scaletransform drawing;clipRect/clipPathclip it.
`shouldRepaint(oldDelegate)` decides whether a new painter instance requires repainting; compare the inputs and return false when nothing changed.
For animation, pass the Animation (any Listenable) as the painter's `repaint` argument: MyPainter(progress: controller) : super(repaint: controller). The painter then repaints on every tick without rebuilding any widgets — the most efficient way to animate a drawing.
Give CustomPaint a size via size: or a child or parent constraints; otherwise it may be zero-sized. Wrap it in RepaintBoundary if it repaints often. For hit testing on custom shapes, override hitTest. For accessibility, provide semanticsBuilder or wrap it in Semantics with a label, since the canvas has no semantic content.
Example
import 'dart:math' as math;
import 'package:flutter/material.dart';
class RingPainter extends CustomPainter {
RingPainter({required this.progress, required this.color}) : super(repaint: progress);
final Animation<double> progress; // repaints on every tick, no widget rebuild
final Color color;
@override
void paint(Canvas canvas, Size size) {
final center = size.center(Offset.zero);
final radius = size.shortestSide / 2 - 6;
Paint stroke(Color c) => Paint()
..style = PaintingStyle.stroke..strokeWidth = 12..strokeCap = StrokeCap.round..color = c;
final track = stroke(color.withValues(alpha: 0.2)), arc = stroke(color);
canvas.drawCircle(center, radius, track);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-math.pi / 2, 2 * math.pi * progress.value, false, arc);
}
@override
bool shouldRepaint(RingPainter old) => old.color != color || old.progress != progress;
}
// Usage inside a State with an AnimationController `_c`:
// RepaintBoundary(child: CustomPaint(size: const Size.square(120),
// painter: RingPainter(progress: _c, color: Colors.teal)))Remember
- Draw with Canvas + Paint + Path in a CustomPainter
- Pass an Animation as
repaintto animate without rebuilding widgets - Implement
shouldRepaintcarefully and add semantics
Watch out for
- Returning
truefrom shouldRepaint unconditionally, repainting even when nothing changed.