先来看看某互联网公司前端开发和产品的日常交流(互掐):
很精彩吧,这种故事经常在互联网公司上演,那你可能会问这和本篇文章有什么关系呢?答案是没有关系。
到这里先别急着骂我哈,小编先来捋捋是咋个回事儿,作为一个从来都是和产品和平相处(苦大仇深)的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技术实践,记得关注加点赞哦。