Implementing Custom Painting on the Canvas in Flutter

Flutter provides a robust CustomPaint widget, allowing developers to draw directly onto the screen using a Canvas object. This feature is incredibly powerful for creating custom visualizations, interactive elements, and unique UI components. In this comprehensive guide, we’ll explore how to implement custom painting on the canvas in Flutter with detailed examples and best practices.

Understanding the CustomPaint Widget

The CustomPaint widget is at the heart of custom drawing in Flutter. It allows you to use a CustomPainter to paint on a Canvas. The Canvas class offers a rich set of drawing methods to create shapes, text, images, and more.

Key Components:

  • CustomPaint Widget: The container that manages the painting process.
  • CustomPainter Class: Your custom class that extends CustomPainter. It contains the paint() method where all drawing happens.
  • Canvas Object: Provides the API for drawing shapes, paths, text, and images.
  • Paint Object: Defines the style (color, stroke width, etc.) for drawing.

Implementing Custom Painting

Follow these steps to implement custom painting on the canvas in Flutter:

Step 1: Create a Custom Painter Class

First, create a class that extends CustomPainter. This class needs to override two methods:

  • paint(Canvas canvas, Size size): This is where you’ll write your drawing code. The canvas parameter is your drawing surface, and size provides the dimensions of the widget.
  • shouldRepaint(CustomPainter oldDelegate): Determines whether the painter needs to be redrawn. Return true if the painter should be redrawn, and false otherwise.

import 'package:flutter/material.dart';

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Add your drawing code here
  }

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

Step 2: Drawing on the Canvas

Inside the paint() method, you’ll use the canvas object to draw. Here are some common drawing methods:

  • drawRect(Rect rect, Paint paint): Draws a rectangle.
  • drawCircle(Offset center, double radius, Paint paint): Draws a circle.
  • drawLine(Offset p1, Offset p2, Paint paint): Draws a line.
  • drawPath(Path path, Paint paint): Draws a complex path.
  • drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint): Draws an arc.
  • drawOval(Rect rect, Paint paint): Draws an oval.
  • drawImage(Image image, Offset offset, Paint paint): Draws an image.
  • drawText(TextPainter textPainter, Offset offset): Draws text.

You’ll also need to create a Paint object to define the style of your drawing:


final paint = Paint()
  ..color = Colors.blue
  ..strokeWidth = 5
  ..style = PaintingStyle.stroke; // or PaintingStyle.fill

Example of drawing a circle:


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

    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 4;

    canvas.drawCircle(center, radius, paint);
  }

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

Step 3: Using the CustomPaint Widget

Wrap your CustomPainter with a CustomPaint widget in your Flutter UI:


import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Custom Painting Example'),
        ),
        body: Center(
          child: CustomPaint(
            size: Size(300, 300), // Adjust size as needed
            painter: MyPainter(),
          ),
        ),
      ),
    ),
  );
}

Advanced Examples and Use Cases

Let’s explore more complex examples and practical use cases to showcase the versatility of custom painting in Flutter.

Example 1: Drawing a Path

Drawing a path allows you to create complex shapes using lines, curves, and other path commands.


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

    final path = Path();
    path.moveTo(size.width / 4, size.height / 2); // Move to starting point
    path.quadraticBezierTo(
        size.width / 2, size.height / 4, 
        size.width * 3 / 4, size.height / 2); // Create a curve
    path.lineTo(size.width / 2, size.height * 3 / 4); // Draw a line back to center
    path.close(); // Close the path

    canvas.drawPath(path, paint);
  }

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

Use it in your app:


CustomPaint(
  size: Size(300, 300),
  painter: PathPainter(),
)

Example 2: Drawing Text

You can draw text on the canvas using the TextPainter class. This is useful for creating custom labels or annotations.


import 'package:flutter/rendering.dart';

class TextPainterExample extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final textPainter = TextPainter(
      text: TextSpan(
        text: 'Hello, Flutter!',
        style: TextStyle(
          color: Colors.red,
          fontSize: 24,
        ),
      ),
      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(CustomPainter oldDelegate) {
    return false;
  }
}

Usage:


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

Example 3: Interactive Drawing with Gestures

Combining custom painting with gesture detection allows you to create interactive drawing experiences.


import 'package:flutter/material.dart';

class InteractivePainter extends StatefulWidget {
  @override
  _InteractivePainterState createState() => _InteractivePainterState();
}

class _InteractivePainterState extends State {
  List points = [];

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        setState(() {
          RenderBox box = context.findRenderObject() as RenderBox;
          Offset localPosition = box.globalToLocal(details.globalPosition);
          points = List.from(points)..add(localPosition);
        });
      },
      onPanEnd: (details) => points.add(Offset.infinite),
      child: CustomPaint(
        size: Size.infinite,
        painter: DrawingPainter(points: points),
      ),
    );
  }
}

class DrawingPainter extends CustomPainter {
  DrawingPainter({required this.points});

  final List points;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 3
      ..strokeCap = StrokeCap.round;

    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != Offset.infinite && points[i + 1] != Offset.infinite) {
        canvas.drawLine(points[i], points[i + 1], paint);
      }
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true; // Repaint whenever the points change
  }
}

Now use InteractivePainter in your UI:


Scaffold(
  appBar: AppBar(title: Text('Interactive Drawing')),
  body: InteractivePainter(),
)

Use Cases:

  • Data Visualization: Create custom charts, graphs, and data visualizations.
  • Game Development: Draw game elements, backgrounds, and special effects.
  • Custom UI Controls: Design unique buttons, sliders, and progress bars.
  • Signature Capture: Implement signature capture functionality in your app.
  • Drawing and Painting Apps: Build full-fledged drawing and painting applications.

Best Practices for Custom Painting

  • Optimize shouldRepaint: Ensure shouldRepaint returns the correct value to prevent unnecessary redraws.
  • Use Paint Objects Efficiently: Create and reuse Paint objects to avoid creating new objects in every frame.
  • Cache Complex Drawings: If you have complex drawings that don't change often, cache them into an Image and redraw the image instead.
  • Use Layering: Use canvas layering (saveLayer, restore) for complex compositions to manage rendering efficiently.
  • Consider Performance: Complex painting can be performance-intensive, especially on older devices. Test and optimize your code for smooth performance.

Conclusion

Implementing custom painting on the canvas in Flutter provides a versatile way to create visually appealing and interactive UIs. With the CustomPaint widget, CustomPainter class, and the powerful Canvas API, you can draw almost anything you can imagine. By following the examples and best practices in this guide, you’ll be well-equipped to build stunning and unique Flutter applications.