Flutter绘制-10-CustomPaint与CustomPainter详解

4,661 阅读6分钟

查看目录-->

基础使用

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()。
简单示例

image.png

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。

给最上面的例子做个变种,添加个动画,效果如下:

代码如下:

paint.gif

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,用于与动画建立监听关系。

至于动画,后面在记录。