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
Canvasobject provides the drawing surface, and theSizeobject 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
shouldRepaintmethod 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.