Custom Painting in Flutter: Draw Your Own Widgets

Custom Painting in Flutter: Draw Your Own Widgets

What You’ll Learn

Learn how to use CustomPaint and CustomPainter to create custom graphics and visual elements beyond Flutter’s built-in widgets—perfect for charts, diagrams, and unique UI elements in desktop apps.

Why Custom Painting Matters

Sometimes standard Flutter widgets aren’t enough. Maybe you need a custom progress indicator, a hand-drawn chart, or a unique shape. That’s where CustomPaint comes in. It gives you direct access to the canvas to draw exactly what you need.

The Basics

Custom painting involves two key components:

Example: Drawing a Simple Progress Arc

Let’s create a circular progress indicator with a custom style:

import 'package:flutter/material.dart';
import 'dart:math';

class ArcProgressPainter extends CustomPainter {
  final double progress; // 0.0 to 1.0
  final Color color;

  ArcProgressPainter({required this.progress, required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = min(size.width, size.height) / 2;

    // Background circle
    final bgPaint = Paint()
      ..color = Colors.grey.shade200
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8;

    canvas.drawCircle(center, radius, bgPaint);

    // Progress arc
    final progressPaint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -pi / 2, // Start at top
      2 * pi * progress, // Sweep angle
      false,
      progressPaint,
    );
  }

  @override
  bool shouldRepaint(ArcProgressPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.color != color;
  }
}

// Usage
class ProgressWidget extends StatelessWidget {
  final double progress;

  const ProgressWidget({Key? key, required this.progress}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: const Size(100, 100),
      painter: ArcProgressPainter(
        progress: progress,
        color: Colors.blue,
      ),
    );
  }
}

Key Methods Explained

Common Canvas Methods

canvas.drawCircle(center, radius, paint);
canvas.drawRect(rect, paint);
canvas.drawLine(p1, p2, paint);
canvas.drawPath(path, paint);
canvas.drawArc(rect, startAngle, sweepAngle, useCenter, paint);

Try It Yourself

Enhance the progress indicator:

  1. Add a percentage text in the center using TextPainter
  2. Animate the progress from 0 to 1 using AnimationController
  3. Make it respond to mouse hover on desktop (change color/size)

Hint for text:

final textPainter = TextPainter(
  text: TextSpan(text: '${(progress * 100).toInt()}%'),
  textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(canvas, /* position */);

Tip of the Day

When working with CustomPaint, use RepaintBoundary to isolate expensive paint operations and improve performance. Wrap your CustomPaint widget:

RepaintBoundary(
  child: CustomPaint(painter: MyPainter()),
)

This prevents unnecessary repaints of other parts of your widget tree when only your custom painting changes—especially valuable for desktop apps with complex UIs!