基础使用
CustomPaint的属性
构造函数:
const CustomPaint({
Key? key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
}
- painter,背景绘制
- child,夹在painter和foregroundPainter之间的widget
- foregroundPainter,前景绘制
- size,指定绘制区域的大小(我是用web调试的,但是无论如何指定,好像都没作用,后续待看)
- isComplex:是否复杂的绘制,如果是,Flutter会应用一些缓存策略来减少重复渲染的开销。
- willChange:和isComplex配合使用,当启用缓存时,该属性代表在下一帧中绘制是否会改变。
CustomPainter的方法
这是就是真正动手绘制的地方了,CustomPainter是个抽象类,因此需要定义一个类来基础CustomPainter,子类主要override两个方法:
- paint(Canvas canvas, Size size),在该方法内部绘制
- shouldRepaint(covariant CustomPainter oldDelegate),用于判断是否需要重新绘制。注意这里的oldDelegate是前一帧的自己,当当前的自己与之前的自己有不一样的地方时需要重新绘制。
- CustomPainter的构造方法中有个可选命名参数Listenable repaint,当不为空时,当其值发生变化时,会触发paint方法。比如AnimationController,_controler可以当参数传递进去,然后再动画过程中,value每变一次会调用一次paint()。
简单示例
import 'package:flutter/material.dart';
class C15CustomPaint extends StatefulWidget {
@override
_C15CustomPaintState createState() => _C15CustomPaintState();
}
class _C15CustomPaintState extends State<C15CustomPaint> {
@override
Widget build(BuildContext context) {
return Container(
child: SizedBox(
width: 400,
height: 400,
child: CustomPaint(
// size: MediaQuery.of(context).size,
size:Size(300,300),
painter: BgPainter(),
foregroundPainter: ForePainter(),
child: Center(
child: SizedBox(
child: Text("aaaaaaaaaaa"),
width: 300,
height: 300,
),
),
),
),
);
}
}
class BgPainter extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
print("=="+size.width.toString());
Paint paint = Paint()
..color= Colors.grey
..isAntiAlias = true;
canvas.drawPaint(paint);
canvas.clipRect(Rect.fromLTWH(0, 0, size.width, size.height));
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
class ForePainter extends CustomPainter{
@override
void paint(Canvas canvas, Size size) {
Rect rect = Rect.fromCenter(center: Offset(300,200), width: 200, height: 200);
Paint paint = Paint()
..color = Colors.greenAccent
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
所遇到的问题(使用chrome调试):
- size属性好像不起作用
- 给CustomPaint外部套上SizedBox也不起作用
源码分析
CustomPaint
继承关系:CustomPant-SingleChildRenderObjectWidget-RenderObjectWidget-Widget
源码:
class CustomPaint extends SingleChildRenderObjectWidget {
const CustomPaint({
Key? key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget? child,
}) : assert(size != null),
assert(isComplex != null),
assert(willChange != null),
assert(painter != null || foregroundPainter != null || (!isComplex && !willChange)),
super(key: key, child: child);
final CustomPainter? painter;
final CustomPainter? foregroundPainter;
final Size size;
final bool isComplex;
final bool willChange;
// 创建绘制的renderObject时,实际上是落在RenderCustomPaint身上,可以理解为CustomPaint是为RenderCustomPaint收集数据信息的
@override
RenderCustomPaint createRenderObject(BuildContext context) {
return RenderCustomPaint(
painter: painter,
foregroundPainter: foregroundPainter,
preferredSize: size,
isComplex: isComplex,
willChange: willChange,
);
}
// 在更新时,其实更新的是renderObject上的属性,因为renderObject只创建一次
@override
void updateRenderObject(BuildContext context, RenderCustomPaint renderObject) {
renderObject
..painter = painter
..foregroundPainter = foregroundPainter
..preferredSize = size
..isComplex = isComplex
..willChange = willChange;
}
// 当该CustomPaint从widget树unmount时,将其renderObject的painter和foregroundPainter 置空,清理掉。
@override
void didUnmountRenderObject(RenderCustomPaint renderObject) {
renderObject
..painter = null
..foregroundPainter = null;
}
}
注意:RenderCustomPaint createRenderObject()方法,返回的是一个RenderCustomPaint,从这里可以看出,RenderCustomPaint最终必然继承自RenderObject,继承关系如是,RenderCustomPaint-RenderProxyBox-RenderBox-RenderObject。
而createRenderObject()方法其实是RenderObjectWidget中定义的,CustomPaint中重写了该方法:
- CustomPaint间接继承RenderObjectWidget,并重写createRenderObject方法。
- RenderCustomPaint 间接继承RenderObject,所以在CustomPaint的createRenderObject方法中返回RenderCustomPaint是没有问题的。
那接下来看RenderCustomPaint中到底干了什么。
RenderCustomPaint
先看构造方法:
RenderCustomPaint({
CustomPainter? painter,
CustomPainter? foregroundPainter,
Size preferredSize = Size.zero,
this.isComplex = false,
this.willChange = false,
RenderBox? child,
})
跟CustomPaint如出一辙,属性一样,通过CustomPaint传递进来。RenderCustomPaint的作用是处理CustomPainter的调用逻辑关系,包括何时paint,何时调用shouldRepaint方法等。而CustomPainter的关注点是paint,也就是怎么画。
在源码中重点关注两个方法:
- _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter)
- void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter)
_didUpdatePainter 的调用时机是set painter和foregroundPaint时,这时更新painter,注意这里会保留旧的painter,然后调用painter的shouldRepaint方法并将旧的painter传递进来,以便二者进行比较。而set painter和foregroundPaint的时机是创建RenderCustomPaint和更新RenderCustomPaint时。看源码如下:
set painter(CustomPainter? value) {
if (_painter == value)
return;
final CustomPainter? oldPainter = _painter;
_painter = value;
_didUpdatePainter(_painter, oldPainter);//这里
}
set foregroundPainter(CustomPainter? value) {
if (_foregroundPainter == value)
return;
final CustomPainter? oldPainter = _foregroundPainter;
_foregroundPainter = value;
_didUpdatePainter(_foregroundPainter, oldPainter);//这里
}
void _didUpdatePainter(CustomPainter? newPainter, CustomPainter? oldPainter) {
// Check if we need to repaint.
if (newPainter == null) {
assert(oldPainter != null); // We should be called only for changes.
markNeedsPaint();
} else if (oldPainter == null ||
newPainter.runtimeType != oldPainter.runtimeType ||
newPainter.shouldRepaint(oldPainter)) {//这里
markNeedsPaint();
}
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
}
大家有没注意到最后:
if (attached) {
oldPainter?.removeListener(markNeedsPaint);
newPainter?.addListener(markNeedsPaint);
}
当RenderCustomPaint被更新后,对oldPainter进行了清理操作,对newPainter进行了添加操作。
再看_paintWithPainter:
void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
..........
if (offset != Offset.zero)
canvas.translate(offset.dx, offset.dy);
painter.paint(canvas, size);
.........
}
可见,最终我们自定义CustomPainter的paint方法,是落到这里进行调用的。而_paintWithPainter方法是在当前RenderCustomPaint的paint方法中调用的,其实最终都是重写的RenderObject的paint方法。
CustomPainter
先看源码:
abstract class CustomPainter extends Listenable {
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
final Listenable? _repaint;
@override
void addListener(VoidCallback listener) => _repaint?.addListener(listener);
@override
void removeListener(VoidCallback listener) => _repaint?.removeListener(listener);
void paint(Canvas canvas, Size size);
bool shouldRebuildSemantics(covariant CustomPainter oldDelegate) => shouldRepaint(oldDelegate);
bool shouldRepaint(covariant CustomPainter oldDelegate);
bool? hitTest(Offset position) => null;
@override
String toString() => '${describeIdentity(this)}(${ _repaint?.toString() ?? "" })';
}
我们最常用的是paint和shouldrepaint方法。另外还有个hitTest方法,用于判断当前绘制内容的点击测试,返回true表示点中了,否则没点钟。入参是个offset,明显就是点击的那个点,然后根据实际情况去处理这个点是否落在了绘制区域内。
大家注意到没有,CustomPainter竟然是一个Listenable,why?
那Listenable是啥?
abstract class Listenable {
const Listenable();
factory Listenable.merge(List<Listenable?> listenables) = _MergingListenable;
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
}
Listenable源码好像并不能直观的提供给我们有用的信息,但其注释很明确:
/// An object that maintains a list of listeners.
///
/// The listeners are typically used to notify clients that the object has been
/// updated.
///
/// There are two variants of this interface:
///
/// * [ValueListenable], an interface that augments the [Listenable] interface
/// with the concept of a _current value_.
///
/// * [Animation], an interface that augments the [ValueListenable] interface
/// to add the concept of direction (forward or reverse).
大体是说,Listenable维护了一个监听列表,当这些监听列表监听的对象发生变化时,会通知这些监听者。而Animation就是一个典型的例子。
那回过头来,CustomPainter这个Listenable究竟是怎么运作的?
注意构造方法:const CustomPainter({ Listenable? repaint }) : _repaint = repaint;其中repaint,是可选命名参数,类型竟然也是Listenable。
给最上面的例子做个变种,添加个动画,效果如下:
代码如下:
import 'package:flutter/material.dart';
class C15CustomPaint extends StatefulWidget {
@override
_C15CustomPaintState createState() => _C15CustomPaintState();
}
class _C15CustomPaintState extends State<C15CustomPaint>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
new AnimationController(vsync: this, duration: Duration(seconds: 2));
_controller.repeat();
}
@override
Widget build(BuildContext context) {
return Container(
child: SizedBox(
width: 400,
height: 400,
child: CustomPaint(
size: Size(300, 300),
painter: BgPainter(_controller),
foregroundPainter: ForePainter(),
child: Center(
child: SizedBox(
child: Text("aaaaaaaaaaa"),
width: 300,
height: 300,
),
),
),
),
);
}
}
class BgPainter extends CustomPainter {
Animation<double> animation;
@override
void paint(Canvas canvas, Size size) {
print("==" + size.width.toString());
Paint paint = Paint()
..color = Colors.blueAccent
..isAntiAlias = true;
// canvas.drawPaint(paint);
Rect rect = Rect.fromCenter(
center: Offset(100, 200),
width: 200 * animation.value,
height: 200 * animation.value);
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
print("shouldRepaint");
return true;
}
BgPainter(this.animation) : super(repaint: animation) {
print("BgPainter");
}
}
class ForePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Rect rect =
Rect.fromCenter(center: Offset(300, 200), width: 200, height: 200);
Paint paint = Paint()
..color = Colors.greenAccent
..style = PaintingStyle.fill;
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
我把创建的AnimationController传递给了CustomPainter,然后在CustomPainter的构造方法中通过super赋值给了_paint,注意构造方法:
/// Creates a custom painter.
///
/// The painter will repaint whenever `repaint` notifies its listeners.
const CustomPainter({ Listenable? repaint }) : _repaint = repaint;
也就是说,无论何时AnimationController发生了变化,都会造成paint方法的重新调用。这样,动画就与CustomPainter的paint()方法联动起来了。大家可以试下,如果在构造方法中不super,动画是不起作用的。
综上来看,据我理解,CustomPainter有两大部分作用:
- paint和shouldRepaint用于RenderCustomPaint中,起绘制作用
- _repaint,用于与动画建立监听关系。
至于动画,后面在记录。