一、初识Canvas:什么是Canvas、Canvas与Widget的关系、CustomPaint组件详解
1.1 到底什么是Canvas?
先问大家一个问题:你平时用Flutter写界面,是不是都在拼Widget?
嗯,这没错。但你想过没有——那些圆角、阴影、渐变,底层是怎么画出来的?
说白了,Canvas就是一张画布。你告诉它「在这里画一条线」「在那里画一个圆」,它就老老实实给你画出来。我刚开始接触Flutter自定义绘制时,总觉得Canvas很神秘,后来发现它其实就是个「绘图指令的集合」。
举个例子:
canvas.drawCircle(Offset(100, 100), 50, paint);
这行代码的意思很简单:在坐标(100, 100)的位置,画一个半径50的圆,用指定的画笔。你看,是不是很直观?
我在项目中遇到过这样一个场景:产品要求做一个波浪进度条,用普通Widget堆叠简直要命。后来用Canvas画了个贝塞尔曲线,十几行代码就搞定了。那一刻我才真正理解——Canvas是Flutter绘制的底层基石。
核心理解:Canvas不存储任何图形数据,它只负责「执行绘制命令」。每次重绘,所有命令都会重新执行一遍。
1.2 Canvas与Widget的关系
很多初学者会问:我平时用的Container、Text这些Widget,跟Canvas有什么关系?
答案是:所有Widget最终都要靠Canvas来渲染。
你想想看,Flutter的渲染流程是这样的:
- Widget树构建
- Element树同步
- RenderObject布局
- 最终通过Canvas绘制到屏幕上
也就是说,你写的每一个Container,底层都会生成对应的绘制指令,交给Canvas去执行。只不过这些细节被Flutter封装好了,你看不到而已。
我个人习惯把Widget和Canvas的关系比作「乐高积木 vs 3D打印」:
- Widget:像乐高,用现成的积木块拼装,方便但有限制
- Canvas:像3D打印,你想画什么就画什么,自由度极高
举个例子,你想画一个自定义的仪表盘:
class DashboardWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: DashboardPainter(),
size: Size(200, 200),
);
}
}
class DashboardPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 这里你可以用Canvas画任何东西
final paint = Paint()
..color = Colors.blue
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
canvas.drawCircle(size.center(Offset.zero), size.width / 2, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
你看,通过CustomPaint这个「桥梁」,我们就能直接操作Canvas了。
我的建议:能用Widget搞定的,优先用Widget。只有遇到Widget搞不定的复杂图形,才考虑用Canvas。别为了炫技而滥用自定义绘制。
1.3 CustomPaint组件详解
CustomPaint是Flutter提供的一个「画板组件」。它有三个核心参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| painter | CustomPainter? | 前景绘制器,在child之上绘制 |
| foregroundPainter | CustomPainter? | 背景绘制器,在child之下绘制 |
| child | Widget? | 子组件,位于两个绘制层之间 |
这里有个细节要注意:painter和foregroundPainter的区别。我曾经踩过这个坑——想在文字上面画一个遮罩层,结果用了painter,遮罩被文字盖住了。后来才发现,应该用foregroundPainter。
来看一个完整的例子:
class MyCustomPaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
// 背景绘制:画一个渐变矩形
painter: BackgroundPainter(),
// 中间放一个文本
child: Center(
child: Text(
'Hello Canvas',
style: TextStyle(fontSize: 24, color: Colors.white),
),
),
// 前景绘制:画一个半透明遮罩
foregroundPainter: ForegroundPainter(),
size: Size(300, 200),
);
}
}
class BackgroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final gradient = LinearGradient(
colors: [Colors.blue, Colors.purple],
);
final paint = Paint()..shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
class ForegroundPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.black.withOpacity(0.3);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
注意:CustomPaint的size参数如果不指定,会尽可能填满父容器。但如果父容器没有约束,size会变成(0,0),导致什么都画不出来。我刚开始学的时候就犯过这个错,调试了半天才发现是size没设置。
1.4 关于shouldRepaint
这个方法是CustomPainter里必须实现的。它决定了什么时候需要重绘。
简单来说:
- 如果绘制内容不变,返回
false——避免不必要的重绘 - 如果绘制内容变了,返回
true——触发重绘
举个例子,如果你画的是一个静态图形,直接返回false就行:
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
但如果你画的是动画,就需要根据新旧数据判断:
class AnimatedPainter extends CustomPainter {
final double progress;
AnimatedPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
// 根据progress绘制不同状态
}
@override
bool shouldRepaint(covariant AnimatedPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
性能小贴士:shouldRepaint返回true时,整个painter的paint方法会重新执行。如果绘制逻辑很复杂,记得做性能优化。我一般会用RepaintBoundary来隔离重绘区域,避免整个页面都跟着重绘。
1.5 小结
好了,这一章的内容就这些。我们来回顾一下:
- Canvas:一张画布,负责执行绘制命令
- Widget与Canvas的关系:Widget是封装好的积木,Canvas是底层的画笔
- CustomPaint:连接Widget和Canvas的桥梁,有三个核心参数
下一章,我们会深入Canvas的绘制API,从最简单的线条开始,一步步画出复杂的图形。到时候我会分享一些我在实际项目中总结的「绘制套路」,保证让你少走弯路。
嗯,今天就到这里。有什么问题,欢迎在评论区交流。