Custom Widgets & Custom Painting

Introduction to CustomPainter

15 min Lesson 3 of 12

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 the Canvas object; Size tells you how large the available area is.
  • shouldRepaint(CustomPainter oldDelegate) — called before each potential repaint. Return true if the new painter's data is different from the old one and a redraw is needed; return false to skip the expensive repaint.
Note: 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),
            ),
          ),
        ),
      ),
    );
  }
}
Tip: 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.
  • stylePaintingStyle.fill fills the shape; PaintingStyle.stroke draws only the outline.
  • strokeWidth — thickness of the stroke, in logical pixels.
  • isAntiAlias — smooth curved edges (default true).
  • 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.
Warning: Canvas coordinates originate at the top-left corner of the 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.