说实话,大多数 Flutter 开发者有个明显短板:
👉 只会“拼 Widget”,不会“画 UI”
一旦设计稿稍微离谱一点,比如:
- 波形图
- 不规则背景
- 自定义进度条
- Canvas 动画
很多人第一反应是:
“Flutter 怎么没有现成组件?”
不是没有,是你还没用到 CustomPaint。
这篇我不跟你讲官方文档那套,我用一个更工程化的方式,带你把 CustomPaint 这玩意吃透。
一、先把认知掰正:Flutter 本质就是一块画布
很多人学 Flutter,会有种违和感:
“怎么感觉不像原生?”
你没感觉错。
Flutter 本质上就是:
👉 一块高性能 Canvas + 一堆 UI 封装
你平时写的:
Container()
Text()
Row()
本质上都是在“帮你画”。
换个更接地气的理解
我一般跟新人这么解释:
Flutter = 画室
Widget = 画笔
CustomPaint = 你自己拿笔画
当你理解到这一步,CustomPaint 就不再神秘了。
二、什么时候你必须用 CustomPaint?
一句话:
官方组件满足不了,就该你自己画了
典型场景:
- 特殊 UI(设计稿还原)
- 图表 / 波形 / 动画
- 高性能绘制(比堆 Widget 更轻)
三、CustomPaint 到底干了什么?
核心就一句话:
👉 给你一块画布,让你自己画
但它有个前提:
你必须提供一个“画家”(CustomPainter)
四、真正关键:CustomPainter 才是核心
你不能直接用 CustomPainter,因为:
👉 它是抽象类
必须自己实现。
标准写法(工程通用模板)
class WavePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 真正绘制的地方
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
两个方法,讲人话解释一下
1️⃣ paint(你真正干活的地方)
- canvas:画布
- size:当前画布大小
👉 所有图形,都是在这里画出来的
2️⃣ shouldRepaint(性能开关)
很多人乱写这里,这是个性能坑。
👉 它决定要不要重新绘制
经验规则:
- 静态 UI →
false - 动态变化 →
true
五、开始画:先搞一支“画笔”
在 Canvas 世界里,没有画笔你什么都干不了。
final paint = Paint()
..color = Colors.white
..strokeWidth = 3
..strokeCap = StrokeCap.round;
这个东西本质就是:
👉 你所有绘制风格的控制器
可以控制:
- 颜色
- 粗细
- 填充 / 描边
- 抗锯齿
六、实战:画一个“音频波形”
这段是整篇最有价值的地方,我帮你重新讲清楚逻辑。
核心思路
- 找中线(y轴中心)
- 随机生成高度
- 画一根根竖线
- 横向平移
示例代码(已重构表达)
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white
..strokeWidth = 3
..strokeCap = StrokeCap.round;
final centerY = size.height / 2;
double x = 0;
final values = List.generate(100, (_) {
return Random().nextDouble() * centerY;
});
for (final v in values) {
if (x > size.width) break;
canvas.drawLine(
Offset(x, centerY - v),
Offset(x, centerY + v),
paint,
);
x += 6; // 线宽 + 间距
}
}
七、这段代码真正干了啥?(重点)
很多教程到这里就结束了,但新人其实没懂。
我帮你拆开讲👇
1️⃣ 坐标系:从左上角开始
Canvas 的坐标系是:
👉 左上角 = (0,0)
往右是 x+
往下是 y+
2️⃣ 为什么要 centerY?
final centerY = size.height / 2;
👉 用来让波形“居中”
否则你画出来会贴在顶部。
3️⃣ 为什么高度不会超出?
Random().nextDouble() * centerY;
因为:
nextDouble()∈ [0,1)- 最大只会到 centerY
👉 所以不会画出画布
4️⃣ drawLine 本质
canvas.drawLine(start, end, paint);
其实就是:
👉 从 A 点画到 B 点
我们做的是:
- 上:centerY - v
- 下:centerY + v
👉 形成一根“柱状线”
5️⃣ x += 6 是干嘛的?
很多人看不懂这个。
👉 控制横向间距
等价于:
- 线宽:3
- 间距:3
6️⃣ 为什么要限制 width?
if (x > size.width) break;
👉 防止画出画布(性能 + 安全)
八、尺寸问题:为什么默认是 300x300?
Flutter 默认给你一个:
👉 300 × 300 的画布
如果你不控制,它就这么大。
正确做法
Container(
width: 400,
height: 100,
child: CustomPaint(
painter: WavePainter(),
),
)
九、很多人踩的坑(经验总结)
❌ 1. 以为 CustomPaint 很重
其实相反:
👉 比堆 Widget 更轻
❌ 2. shouldRepaint 全写 true
结果:
👉 每帧重绘,直接掉帧
❌ 3. 不控制绘制范围
👉 画出边界 = 渲染异常
❌ 4. 想用它替代一切
别极端:
- 普通 UI → Widget
- 特殊绘制 → CustomPaint
十、最后讲点“工程经验”
CustomPaint 这个东西,本质上是:
Flutter 给你开的“后门能力”
你一旦掌握它:
- UI 上限直接提升
- 不再被组件限制
- 能做真正差异化设计
一句话总结
不会 CustomPaint 的 Flutter 开发,本质上还停留在“拼积木阶段”