In Flutter, how do the CustomPaint widget and a CustomPainter work together to draw custom graphics, and what must a CustomPainter implement?
answer
- widget hosts, delegate draws
- paint(Canvas canvas, Size size)
- shouldRepaint(covariant oldDelegate)
- painter, child, foregroundPainter order
- origin at the box's top left
basics
~20 sCustomPaint is the widget that sizes and hosts the drawing; a CustomPainter is the delegate that draws. A painter implements paint(Canvas, Size) to draw inside that size, and shouldRepaint(oldDelegate) to say whether a new instance needs a repaint.
solid answer
~40 s`CustomPaint` is a widget backed by a `RenderCustomPaint` render object; it lays out (to its child's size, or to its `size` argument within its constraints when there is no child) and calls into its painters during the paint phase. A `CustomPainter` must implement `paint(Canvas canvas, Size size)`, where the canvas origin is the top-left of the box and `size` is its laid-out size, and `shouldRepaint(covariant CustomPainter oldDelegate)`, which is asked whenever a new painter instance replaces the old one. It can optionally override `hitTest`, `semanticsBuilder` and `shouldRebuildSemantics`, and it can take a `repaint` listenable to repaint without a rebuild. `CustomPaint` paints its `painter` first, then its `child`, then its `foregroundPainter`, so you can draw behind or on top of existing widgets.
code
dart · 24 linesclass RingPainter extends CustomPainter {
RingPainter({required this.progress, required this.color});
final double progress; // 0.0 - 1.0
final Color color;
@override
void paint(Canvas canvas, Size size) {
final stroke = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round;
final rect = (Offset.zero & size).deflate(4);
canvas.drawArc(rect, -math.pi / 2, 2 * math.pi * progress, false, stroke);
}
@override
bool shouldRepaint(RingPainter oldDelegate) =>
oldDelegate.progress != progress || oldDelegate.color != color;
}
// Usage (with import 'dart:math' as math;):
// CustomPaint(size: const Size.square(64), painter: RingPainter(progress: 0.6, color: Colors.teal))go deeper
Recall the split: CustomPaint is the widget, CustomPainter draws, and a painter implements paint and shouldRepaint.
Explain how CustomPaint sizes itself with and without a child, the painter-child-foreground order, and why paint runs after layout with a final size.
Keep painters as pure functions of their fields, prepare data outside paint, and add semantics for anything that carries meaning.
Decide when custom painting is worth its maintenance and accessibility cost over composing existing widgets or adopting a charting package.
## Two objects, two jobs Flutter's custom drawing is split between a widget and a delegate: - **`CustomPaint`** (a widget) takes part in the widget tree and layout. It creates a `RenderCustomPaint` render object, which decides the box's size and, in the paint phase, hands the canvas to the painters. - **`CustomPainter`** (a plain Dart class you subclass) holds the drawing logic and the data it draws. It is not a widget and has no `build` method. This separation lets you keep painting logic in a small, testable class and reuse it in different layouts. ## What a CustomPainter implements | Member | Required | Purpose | |---|---|---| | `paint(Canvas canvas, Size size)` | yes | draw into the box; origin at its top-left, extent `size` | | `shouldRepaint(covariant CustomPainter oldDelegate)` | yes | when a new painter instance arrives, return `true` if it draws something different | | `hitTest(Offset position)` | no | decide whether a pointer at `position` hits the drawing | | `semanticsBuilder` | no | describe the drawing to screen readers | | `shouldRebuildSemantics(oldDelegate)` | no | defaults to `shouldRepaint` | The constructor accepts an optional `repaint` `Listenable`: whenever it notifies, the render object repaints without rebuilding or relaying out anything. ## What CustomPaint takes ```dart CustomPaint( painter: GridPainter(), // paints behind the child foregroundPainter: MarkerPainter(), // paints on top of the child size: const Size(200, 120), // used only when there is no child child: const Text('Label'), ) ``` - **Paint order:** `painter`, then `child`, then `foregroundPainter`. - **Sizing:** with a child, the box takes the child's size and `size` is ignored. Without a child it tries to be `size` (default `Size.zero`) within its constraints. - **Hints:** `isComplex` and `willChange` are hints to the compositor about caching; they default to `false`. ## Drawing inside paint The `Canvas` API comes from `dart:ui`. Typical calls: 1. Create a `Paint` describing **how** to draw: `color`, `style` (`PaintingStyle.fill` by default, or `stroke`), `strokeWidth`, `strokeCap`, `shader`. 2. Describe **what** to draw: a `Rect`, `RRect`, `Offset`s, or a `Path` built with `moveTo`, `lineTo`, `quadraticBezierTo`, `cubicTo` and `close`. 3. Issue the draw: `drawRect`, `drawCircle`, `drawLine`, `drawPath`, `drawPoints`, or text through a `TextPainter`. 4. Use `save`/`restore` around transforms (`translate`, `rotate`, `scale`) and clips (`clipRect`, `clipRRect`, `clipPath`), and keep them balanced. Everything is expressed in logical pixels relative to the box. Drawing outside `size` is not guaranteed to be clipped; call `canvas.clipRect(Offset.zero & size)` first if data could spill over. ## Where it fits in the frame `paint` runs during the **paint phase**, after layout, so `size` is final. It records drawing commands into a picture layer; nothing reaches the screen until the engine rasterizes that frame. Do not do expensive work such as decoding images or loading files inside `paint`; prepare data beforehand and pass it into the painter. ## Text, images and taps - **Text** is drawn with a `TextPainter`: give it a `TextSpan` and a `textDirection`, call `layout`, then `paint(canvas, offset)`. - **Images** must already be decoded. The docs describe resolving an `ImageProvider` to an `ImageStream`, creating a new painter when its `ImageInfo` arrives, and drawing with `drawImage`, `drawImageRect` or `drawImageNine`, applying `ImageInfo.scale`. - **Taps** are decided by `hitTest(Offset)`. Returning `null` keeps the default: every point hits a background painter and no point hits a foreground painter. Return `true` or `false` to hit-test only the drawn shape, for example with `path.contains(position)`. ## Common uses - charts, sparklines, gauges and progress rings; - signatures and drawing pads, fed by gesture positions; - decorative shapes that no built-in widget offers; - overlays such as selection marks drawn with `foregroundPainter` on top of an image.
- When would you use foregroundPainter instead of painter?When the drawing must appear on top of the child, for example selection handles over an image or a highlight over text. `painter` draws before the child, so the child would cover it. By default a foreground painter's `hitTest` treats no point as a hit, so it does not steal taps from the child.
- A chart drawn with CustomPainter is invisible to screen readers. How do you fix that?A painter contributes nothing to the semantics tree unless it overrides `semanticsBuilder` to return `CustomPainterSemantics` entries with a rect and properties such as a label. Alternatively wrap the `CustomPaint` in a `Semantics` widget with a summary label, which is often enough for a simple chart.
saying these in an interview costs you the question
- CustomPainter is a widget with its own build method.
- The canvas origin in paint is the top-left of the screen.
- CustomPaint always clips whatever the painter draws outside its size.
- foregroundPainter paints underneath the child widget.
- With a child present, CustomPaint's size argument sets the box size.