如何在Flutter中使用CustomPainter实现自定义绘制?
在 Flutter 中,
CustomPainter是实现自定义绘制的核心组件,可灵活绘制图形、路径、文本、渐变甚至复杂动效,其核心逻辑是通过重写paint()(定义绘制逻辑)和shouldRepaint()(控制重绘时机)来实现自定义视觉效果。以下是从基础到进阶的完整实现指南:一、核心概念与基础流程
1. 核心类说明
| 类 / 对象 | 作用 |
|---|---|
CustomPainter |
抽象类,需继承并实现paint()和shouldRepaint(),封装绘制逻辑 |
Canvas |
绘制画布,提供绘制点、线、矩形、路径、文本、图像等所有绘制 API |
Paint |
画笔,定义颜色、线条宽度、填充方式、抗锯齿、渐变等绘制样式 |
CustomPaint |
Flutter 组件,承载CustomPainter,将绘制内容渲染到界面上 |
2. 基础实现步骤
步骤 1:继承
CustomPainter,实现核心方法创建自定义绘制类,重写paint()(绘制逻辑)和shouldRepaint()(重绘判断)。dart
import 'package:flutter/material.dart';
// 自定义绘制器(绘制一个带渐变的圆形)
class MyCustomPainter extends CustomPainter {
// 可自定义参数,灵活控制绘制效果
final double radius;
final Color primaryColor;
MyCustomPainter({required this.radius, required this.primaryColor});
// 核心:绘制逻辑(Canvas是画布,Size是CustomPaint的尺寸)
@override
void paint(Canvas canvas, Size size) {
// 1. 创建画笔
final Paint paint = Paint()
..color = primaryColor // 基础颜色
..style = PaintingStyle.fill // 填充模式(stroke为描边)
..isAntiAlias = true; // 抗锯齿
// 2. 进阶:添加渐变(替代单一颜色)
paint.shader = RadialGradient(
center: Alignment.center,
radius: 1.0,
colors: [primaryColor, primaryColor.withOpacity(0.3)],
).createShader(Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2),
radius: radius,
));
// 3. 绘制圆形(画布中心为圆心)
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), // 圆心坐标
radius, // 半径
paint, // 画笔
);
}
// 关键:判断是否需要重绘(优化性能)
// 仅当绘制参数变化时返回true,避免无意义重绘
@override
bool shouldRepaint(covariant MyCustomPainter oldDelegate) {
return oldDelegate.radius != radius || oldDelegate.primaryColor != primaryColor;
}
}
步骤 2:通过
CustomPaint组件渲染将自定义CustomPainter传入CustomPaint的painter参数,嵌入 Flutter 界面树中。dart
class CustomPaintDemo extends StatelessWidget {
const CustomPaintDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('自定义绘制基础')),
body: Center(
// 承载自定义绘制的核心组件
child: CustomPaint(
// 绘制区域大小(不设置则自适应子组件/父组件)
size: const Size(200, 200),
// 前景绘制(覆盖子组件)
painter: MyCustomPainter(radius: 80, primaryColor: Colors.blue),
// 可选:背景绘制(被前景/子组件覆盖)
// backgroundPainter: MyBackgroundPainter(),
// 可选:子组件(绘制在画布上层)
child: const Text(
'自定义圆形',
style: TextStyle(fontSize: 16, color: Colors.white),
),
),
),
);
}
}
二、常用绘制 API(Canvas 核心操作)
Canvas提供了丰富的绘制方法,以下是高频使用的场景示例:1. 绘制基础图形
dart
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.red
..strokeWidth = 2
..isAntiAlias = true;
// 1. 绘制点
canvas.drawPoints(
PointMode.points, // 点模式(points/lines/polygon)
[const Offset(50, 50), const Offset(100, 100)], // 点坐标列表
paint..strokeWidth = 10, // 点大小由strokeWidth控制
);
// 2. 绘制直线
canvas.drawLine(
const Offset(50, 50), // 起点
const Offset(150, 150), // 终点
paint..color = Colors.green,
);
// 3. 绘制矩形(普通矩形)
canvas.drawRect(
const Rect.fromLTWH(50, 50, 100, 80), // 左、上、宽、高
paint..color = Colors.yellow..style = PaintingStyle.stroke,
);
// 4. 绘制圆角矩形
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(50, 150, 100, 80),
const Radius.circular(10),
),
paint..color = Colors.purple,
);
// 5. 绘制椭圆(矩形内切椭圆)
canvas.drawOval(
const Rect.fromLTWH(50, 250, 100, 60),
paint..color = Colors.orange,
);
}
2. 绘制路径(Path):复杂自定义图形
Path是绘制不规则图形的核心,支持移动、画线、贝塞尔曲线等操作:dart
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.pink
..style = PaintingStyle.fill
..isAntiAlias = true;
// 1. 创建路径(绘制五角星)
final Path path = Path();
// 计算五角星顶点坐标(中心为画布中心)
double centerX = size.width / 2;
double centerY = size.height / 2;
double outerRadius = 80; // 外圆半径
double innerRadius = 40; // 内圆半径
// 遍历绘制五角星的10个顶点(5个外顶点+5个内顶点)
for (int i = 0; i < 10; i++) {
double angle = i * 36 * pi / 180; // 每个顶点的角度(36°间隔)
double r = i % 2 == 0 ? outerRadius : innerRadius; // 偶索引取外圆,奇索引取内圆
double x = centerX + r * cos(angle);
double y = centerY + r * sin(angle);
if (i == 0) {
path.moveTo(x, y); // 移动到第一个顶点
} else {
path.lineTo(x, y); // 连线到后续顶点
}
}
path.close(); // 闭合路径
// 2. 绘制路径
canvas.drawPath(path, paint);
}
3. 绘制文本与图像
dart
@override
void paint(Canvas canvas, Size size) {
// 1. 绘制文本
final TextPainter textPainter = TextPainter(
text: const TextSpan(
text: 'Flutter自定义绘制',
style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold),
),
textDirection: TextDirection.ltr, // 文本方向(必须指定)
);
// 布局文本(计算尺寸)
textPainter.layout(minWidth: 0, maxWidth: size.width);
// 绘制文本(居中显示)
textPainter.paint(
canvas,
Offset((size.width - textPainter.width) / 2, (size.height - textPainter.height) / 2),
);
// 2. 绘制图像(需提前加载图片)
// 示例:通过ImageProvider加载本地/网络图片
final ImageProvider imageProvider = AssetImage('assets/flutter_logo.png');
// 注意:图像绘制需异步加载,建议结合FutureBuilder或提前缓存
imageProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo info, bool _) {
canvas.drawImage(
info.image,
Offset(size.width / 2 - info.image.width / 2, size.height / 2 + 40),
Paint(),
);
}),
);
}
三、进阶技巧:动效与性能优化
1. 结合动画实现动态绘制
通过
AnimationController和Listener,让绘制参数随动画变化,实现动态效果:dart
class AnimatedCustomPainter extends StatefulWidget {
const AnimatedCustomPainter({super.key});
@override
State<AnimatedCustomPainter> createState() => _AnimatedCustomPainterState();
}
class _AnimatedCustomPainterState extends State<AnimatedCustomPainter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _radiusAnimation;
@override
void initState() {
super.initState();
// 创建动画控制器(2秒循环)
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true); // 反向循环
// 动画值:半径从50到100
_radiusAnimation = Tween<double>(begin: 50, end: 100).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return CustomPaint(
size: const Size(200, 200),
painter: MyCustomPainter(
radius: _radiusAnimation.value,
primaryColor: Colors.blue,
),
);
},
),
);
}
}
2. 性能优化关键
- 精准控制重绘:
shouldRepaint()仅在绘制参数变化时返回true,避免频繁重绘;dart@override bool shouldRepaint(covariant MyCustomPainter oldDelegate) { // 仅当半径/颜色变化时重绘 return oldDelegate.radius != radius || oldDelegate.primaryColor != primaryColor; } - 使用
RepaintBoundary隔离渲染层:若自定义绘制组件与其他动效组件共存,用RepaintBoundary包裹,避免其他组件动效触发绘制重绘;dartRepaintBoundary( child: CustomPaint(painter: MyCustomPainter(radius: 80, primaryColor: Colors.blue)), ) - 减少复杂计算:将绘制中的固定计算(如坐标、渐变)移到
paint()外,避免每次绘制重复计算; - 避免过度抗锯齿:非必要场景关闭
isAntiAlias(默认 false),减少渲染开销。
3. 坐标系变换(平移 / 旋转 / 缩放)
Canvas支持坐标系变换,灵活调整绘制位置和角度:dart
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = Colors.green..strokeWidth = 2;
final Offset center = Offset(size.width / 2, size.height / 2);
// 1. 平移坐标系(画布原点移到中心)
canvas.translate(center.dx, center.dy);
// 2. 旋转坐标系(顺时针旋转45°)
canvas.rotate(pi / 4);
// 3. 缩放坐标系(放大1.5倍)
canvas.scale(1.5);
// 绘制矩形(基于变换后的坐标系)
canvas.drawRect(
const Rect.fromLTWH(-50, -50, 100, 100),
paint..style = PaintingStyle.stroke,
);
// 重置坐标系(避免影响后续绘制)
canvas.restore();
}
四、常见问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 绘制内容不显示 | 1. 检查CustomPaint是否设置size;2. 确认绘制坐标在size范围内;3. 检查画笔颜色与背景是否一致 |
| 文本绘制乱码 / 不显示 | 必须指定TextPainter的textDirection(如TextDirection.ltr) |
| 图像绘制不显示 | 图像加载是异步的,需通过ImageStreamListener或FutureBuilder等待加载完成 |
| 绘制性能差、卡顿 | 1. 优化shouldRepaint();2. 使用RepaintBoundary;3. 减少复杂路径计算 |
五、总结
CustomPainter的核心是通过Canvas操作绘制指令,通过Paint定义样式,通过shouldRepaint控制性能,其使用流程可总结为:- 继承
CustomPainter,封装绘制参数; - 在
paint()中通过Canvas和Paint实现绘制逻辑; - 重写
shouldRepaint()优化重绘; - 通过
CustomPaint组件将绘制内容渲染到界面; - 进阶场景结合动画、坐标系变换实现复杂效果,并做好性能优化。
掌握以上方法,可实现从简单图形到复杂动效的所有自定义绘制需求,比如图表、个性化 UI、游戏画面等。

浙公网安备 33010602011771号