App开发装X指南:玩转自定义绘制
Flutter提供的内置组件的确可以满足大部分UI需求,但有时候需要实现一些特殊的UI效果,比如自定义图形(不规则的图形)、动画、渐变背景等,这时候就需要使用自定义绘制来实现。
先来看看某互联网公司前端开发和产品的日常交流(互掐):
很精彩吧,这种故事经常在互联网公司上演,那你可能会问这和本篇文章有什么关系呢?答案是没有关系。
到这里先别急着骂我哈,小编先来捋捋是咋个回事儿,作为一个从来都是和产品和平相处(苦大仇深)的App前端开发,每次碰到类似这种的需求心里都想对产品问候几遍,但是需要装X的时候,咱们得上啊,比人会的咱也会,别人不会的咱还得会,比如说 Flutter 的自定义绘制。

你可能会问,这玩意儿能干啥? Flutter 的内置组件还不够用吗?是的,Flutter 提供的内置组件的确可以满足大部分UI需求,但有时候需要实现一些特殊的UI效果,比如自定义图形(不规则的图形)、动画、渐变背景等,这时候就需要使用自定义绘制来实现。除了可以高度定制化的 UI 效果,同时可以减少 UI 的层级嵌套,优化 UI 性能,好处是不是很多。

比如上图中显示当前温度的圆形进度条,内置的 Widget 组件就没法儿实现了,这里就需要用到 Flutter 中的 CustomPainter。
CustomPainter 是啥?
CustomPainter 是 Flutter 中的一个抽象类,用于绘制自定义的图形和图像。通过实现 CustomPainter 类并重写其 paint 方法,开发者可以自由地定义绘制逻辑,从而实现各种复杂的绘图效果。下面使用 CustomPainter 来绘制一个简单的自定义图形.
class CustomPainterPagePage extends StatefulWidget {
const CustomPainterPagePage({Key? key}) : super(key: key);
State<CustomPainterPagePage> createState() => _CustomPainterPagePageState();
}
class _CustomPainterPagePageState extends State<CustomPainterPagePage> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffF2F4F5),
body: CustomPaint(
painter: MyCustomPainter(), // 应用自定义的绘制类
child: const SizedBox(
width: 200.0,
height: 200.0,
),
),
);
}
}
class MyCustomPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 3.0
..style = PaintingStyle.fill;
// 绘制一个圆形
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50.0, paint);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
效果:
从上面的例子中可以看到使用 CustomPainter 绘制自定义图形有以下几个步骤:
- 创建一个继承自
CustomPainter的子类MyCustomPainter,并实现其中的paint方法来定义绘图逻辑。在paint方法中,可以使用Canvas API来执行各种绘制操作,如绘制路径、文本、图像等。 - 将自定义的绘制类
MyCustomPainter的实例传递给CustomPaint的painter属性,即可将自定义的绘制逻辑应用到Widget中。
CustomPaint 介绍
下面是 CustomPaint 的构造函数:
const CustomPaint({
super.key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
super.child,
})
child:子节点。painter: 背景画笔,会显示在child后面;foregroundPainter: 前景画笔,会显示在child前面size:当child为null时,代表默认绘制区域大小,如果有child则忽略此参数,画布尺寸则为child尺寸。如果有child但是想指定画布为特定大小,可以使用SizeBox包裹CustomPaint实现。isComplex:是否复杂的绘制,如果是,Flutter会应用一些缓存策略来减少重复渲染的开销。willChange:和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。
CustomPainter 源码
下面是搂出的 CustomPainter 源码,为了好理解,小编在每个函数上面做了注释。
abstract class CustomPainter extends Listenable {
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
final Listenable? _repaint;
// 注册一个回调,以便在需要重新绘制时收到通知。
void addListener(VoidCallback listener) => _repaint?.addListener(listener);
// 用不到的时候,需要移除监听。
void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);
// 子类重写在此方法,并执行各种绘制操作。
void paint(Canvas canvas, Size size);
// 为该绘制的图形构建语义信息。
SemanticsBuilderCallback? get semanticsBuilder => null;
// 是否需要重绘语义信息
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate);
// 是否需要重绘。
bool shouldRepaint(covariant CustomPainter oldDelegate);
// 点击时是否命中,传过来 position 对于当前绘制图形视为命中的点则为true,否则为false
bool? hitTest(Offset position) => null;
String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })';
}
这里面使用频率最高的就是 void paint(Canvas canvas, Size size); 函数了,Canvas 就是画布,Size 是当前绘制区域大小,下面是 Canvas 内部常用的绘制函数。
drawLine划线drawPoint画点drawPath画路径drawImage画图像drawRect画矩形drawCircle画圆drawOval画椭圆drawArc画圆弧
Paint 是画笔,可以配置画笔的各种属性如粗细、颜色、样式等。
final paint = Paint()
..color = Colors.blue // 画笔颜色
..strokeWidth = 3.0 // 画笔线条大小
..isAntiAlias = true //是否抗锯齿
..style = PaintingStyle.fill; //画笔样式:填充
画板刷新
在 CustomPainter 源码中,构造函数中 repaint 是干啥用的?
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
final Listenable? _repaint;
其实 Fultter 源码注释文档已经告诉我们了,

翻译过来就是,触发重绘的最高效方式是:
- 继承
[CustomPainter]类,并在构造函数提供一个'repaint'参数,当需要重新绘制时,该对象会进行通知它的监听者。 - 继承
[Listenable](比如通过[ChangeNotifier])并实现[CustomPainter],这样对象本身就可以直接提供通知。
可能你会问直接 setState 干不就完了吗?还用得着这么麻烦。setState 当然是可以,但咱们是对程序性能有追求的,而且还得根据具体使用的场景。setState 重建的范围太大,如果绘制的是一个大且复杂的自定义图形,加上 CustomPaint 还有一个 child 子节点,亦或者还有一个高频率的动画和滑动,这些情况下用 setState 来销毁再重建 Widget 有可能直接影响页面的流畅度。下面整个例子来实现触发重绘的最高效方式。
class SizeChangedPainter extends CustomPainter {
final Animation<double> animation;
SizeChangedPainter({required this.animation});
void paint(Canvas canvas, Size size) {
// 绘制逻辑
double rectWidth = animation.value * size.width;
double rectHeight = animation.value * size.height;
Paint paint = Paint()..color = Colors.blue;
canvas.drawRect(Rect.fromLTRB(0, 0, rectWidth, rectHeight), paint);
}
bool shouldRepaint(covariant SizeChangedPainter oldDelegate) {
// 默认返回true,表示总是需要重绘
return oldDelegate.animation.value != animation.value;
}
}
页面调用:
class CustomPainterPagePage extends StatefulWidget {
const CustomPainterPagePage({Key? key}) : super(key: key);
State<CustomPainterPagePage> createState() => _CustomPainterPagePageState();
}
class _CustomPainterPagePageState extends State<CustomPainterPagePage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
// TODO: implement initState
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 3),
);
_animation = Tween<double>(begin: 0.2, end: 3.0).animate(_controller);
_controller.forward();
}
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffF2F4F5),
body: Column(
children: [
CustomPaint(
painter: SizeChangedPainter(animation: _animation),
child: const SizedBox(
width: 200.0,
height: 200.0,
),
),
],
),
);
}
}
上面的例子可以看出将 Animation<double> 通过构造函数赋值给成员变量 repaint 。而 repaint 是 Listenable 可监听对象类型,当 repaint 也就是 _animation 值发送变化时,会通知画板调用 paint 实现重绘,效果如下:

好了,先啰嗦到这里了,下篇来实现一个有难度点儿的自定义图形,敬请期待吧,我的公众号:Flutter技术实践,记得关注加点赞哦。
更多推荐



所有评论(0)