Advanced Techniques and Practical Examples of Using CustomPainter in Flutter

Flutter provides a powerful and flexible way to draw custom graphics through the CustomPaint widget and the CustomPainter class. By using CustomPainter, you can create intricate designs, charts, and visual effects that go beyond the standard widgets offered by Flutter. This blog post dives into advanced techniques and practical examples for mastering CustomPainter in Flutter.

What is CustomPainter in Flutter?

The CustomPainter is a class that allows you to paint directly on the canvas. The CustomPaint widget takes a CustomPainter object, which defines how to draw on the canvas. This is particularly useful when you need to create custom graphics or modify existing widgets with unique visual elements.

Why Use CustomPainter?

  • Custom Graphics: Create any type of graphic from scratch.
  • Unique Visual Effects: Add effects like gradients, shadows, and custom shapes.
  • Performance Optimization: Control the painting process to optimize for performance.
  • Complex UI Elements: Design UI elements that are not available in the standard Flutter widgets.

Basic Implementation of CustomPainter

Let’s start with a basic example of creating a simple custom painter:

Step 1: Create a CustomPainter Class

import 'package:flutter/material.dart';

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Define the paint object
    final paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke;

    // Draw a circle
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false; // Return true if the painter needs to be redrawn
  }
}

Explanation:

  • paint: This method is called whenever the custom paint needs to be drawn. The Canvas object provides the drawing surface, and the Size object represents the size of the area being painted.
  • shouldRepaint: This method determines whether the painter needs to be redrawn. If the properties used to draw the graphic have changed, this should return true; otherwise, return false for performance reasons.

Step 2: Use CustomPaint Widget

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('CustomPainter Example')),
        body: Center(
          child: CustomPaint(
            size: Size(200, 200), // Set the size of the painting area
            painter: MyPainter(),   // Use the CustomPainter class
          ),
        ),
      ),
    );
  }
}

Advanced Techniques and Practical Examples

1. Drawing Complex Shapes and Paths

You can create complex shapes using Path objects. Paths allow you to define a series of connected lines, curves, and arcs to form intricate designs.

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

class HeartPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.fill;

    final path = Path();

    // Start point
    path.moveTo(size.width / 2, size.height / 5);

    // Top left curve
    path.cubicTo(
      5 * size.width / 14, 0,
      0, size.height / 15,
      size.width / 28, 2 * size.height / 5,
    );

    // Top right curve
    path.cubicTo(
      size.width / 14, 2 * size.height / 5,
      3 * size.width / 7, 5 * size.height / 12,
      size.width / 2, 5 * size.height / 12,
    );

    // Bottom right curve
    path.cubicTo(
      4 * size.width / 7, 5 * size.height / 12,
      13 * size.width / 14, 2 * size.height / 5,
      27 * size.width / 28, 2 * size.height / 5,
    );

    // Bottom left curve
    path.cubicTo(
      size.width, size.height / 15,
      9 * size.width / 14, 0,
      size.width / 2, size.height / 5,
    );

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Usage:


CustomPaint(
  size: Size(200, 200),
  painter: HeartPainter(),
)

2. Using Gradients and Shaders

Gradients and shaders can add depth and visual interest to your custom graphics.

import 'package:flutter/material.dart';

class GradientPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..shader = LinearGradient(
        colors: [Colors.purple, Colors.blue],
        begin: Alignment.topLeft,
        end: Alignment.bottomRight,
      ).createShader(Rect.fromLTRB(0, 0, size.width, size.height));

    canvas.drawRect(Rect.fromLTRB(0, 0, size.width, size.height), paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Usage:


CustomPaint(
  size: Size(200, 200),
  painter: GradientPainter(),
)

3. Animating CustomPainter

You can animate CustomPainter by using AnimationController and ListenableBuilder to trigger redraws based on animation changes.

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

class AnimatedCirclePainter extends CustomPainter {
  final double animationValue;

  AnimatedCirclePainter({required this.animationValue});

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 5;

    final radius = size.width / 2 * animationValue;
    canvas.drawCircle(Offset(size.width / 2, size.height / 2), radius, paint);
  }

  @override
  bool shouldRepaint(covariant AnimatedCirclePainter oldDelegate) {
    return oldDelegate.animationValue != animationValue;
  }
}

class AnimatedCircle extends StatefulWidget {
  @override
  _AnimatedCircleState createState() => _AnimatedCircleState();
}

class _AnimatedCircleState extends State with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);
    _animation = CurvedAnimation(parent: _controller, curve: Curves.easeInOut);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return CustomPaint(
          size: Size(200, 200),
          painter: AnimatedCirclePainter(animationValue: _animation.value),
        );
      },
    );
  }
}

Usage:


AnimatedCircle(),

4. Drawing Text

You can draw custom text on the canvas using the TextPainter class.

import 'package:flutter/material.dart';

class TextPainterExample extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textStyle = TextStyle(
      color: Colors.black,
      fontSize: 24,
    );
    final textSpan = TextSpan(
      text: 'Hello, CustomPainter!',
      style: textStyle,
    );
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );
    final x = (size.width - textPainter.width) / 2;
    final y = (size.height - textPainter.height) / 2;
    textPainter.paint(canvas, Offset(x, y));
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Usage:


CustomPaint(
  size: Size(300, 100),
  painter: TextPainterExample(),
)

5. Drawing Images

You can draw images using drawImage, drawImageRect, or drawAtlas.


import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class ImagePainter extends CustomPainter {
  ui.Image? image;

  ImagePainter({required this.image});

  @override
  void paint(Canvas canvas, Size size) {
    if (image != null) {
      canvas.drawImage(image!, Offset.zero, Paint());
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

class ImageExample extends StatefulWidget {
  @override
  _ImageExampleState createState() => _ImageExampleState();
}

class _ImageExampleState extends State {
  ui.Image? image;

  @override
  void initState() {
    super.initState();
    _loadImage();
  }

  Future _loadImage() async {
    final data = await rootBundle.load('assets/flutter_logo.png');
    final list = data.buffer.asUint8List();
    final decodedImage = await decodeImageFromList(list);

    setState(() {
      image = decodedImage;
    });
  }

  @override
  Widget build(BuildContext context) {
    return image == null
        ? CircularProgressIndicator()
        : CustomPaint(
            size: Size(200, 200),
            painter: ImagePainter(image: image),
          );
  }
}

Performance Optimization Tips

  • Minimize Repaints: Only repaint when necessary by implementing the shouldRepaint method effectively.
  • Simplify Calculations: Pre-calculate values where possible to reduce computational overhead during the paint method.
  • Use Cache: Cache drawing operations when the same graphic is drawn repeatedly.
  • Optimize Paths: Simplify paths and reduce the number of points for better performance.

Conclusion

CustomPainter is a versatile tool for creating custom graphics and visual effects in Flutter. By mastering advanced techniques like drawing complex shapes, using gradients, animating painters, and optimizing performance, you can elevate the visual appeal and uniqueness of your Flutter applications. Experiment with these examples and adapt them to fit your specific project needs.