skip to content

In Flutter, how do the CustomPaint widget and a CustomPainter work together to draw custom graphics, and what must a CustomPainter implement?

level: juniorimportance: should knowfreq 45%

answer

  1. widget hosts, delegate draws
  2. paint(Canvas canvas, Size size)
  3. shouldRepaint(covariant oldDelegate)
  4. painter, child, foregroundPainter order
  5. origin at the box's top left

basics

~20 s

CustomPaint 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 lines
dart
class 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

for a junior

Recall the split: CustomPaint is the widget, CustomPainter draws, and a painter implements paint and shouldRepaint.

for a middle

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.

for a senior

Keep painters as pure functions of their fields, prepare data outside paint, and add semantics for anything that carries meaning.

for a principal

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.