Flutter 绘制探索 7 | 不使用 CustomPaint 进行绘制 | 七日打卡

2,447 阅读8分钟

零:前言

1. 系列引言

可能说起 Flutter 绘制,大家第一反应就是用 CustomPaint 组件,自定义 CustomPainter 对象来画。Flutter 中所有可以看得到的组件,比如 Text、Image、Switch、Slider 等等,追其根源都是画出来的,但通过查看源码可以发现,Flutter 中绝大多数组件并不是使用 CustomPaint 组件来画的,其实 CustomPaint 组件是对框架底层绘制的一层封装。这个系列便是对 Flutter 绘制的探索,通过测试调试源码分析来给出一些在绘制时被忽略从未知晓的东西,而有些要点如果被忽略,就很可能出现问题。


2.非 CustomPaint 绘制

前面六篇应该对 CustomPaint 组件相关的知识说得淋漓尽致了。但 CustomPaint 在源码中的应用只有大约 20 个组件,绝大多数可视的组件都是其他方式绘制的。当你深入了解这些组件的绘制时,就会发现,无论是 CustomPaint 还是其他组件,它们最终都是基于 RenderObject#paint 进行的绘制操作。那 CustomPaint 相比于 RenderObject#paint 有着什么优劣区别呢?

优势在于:CustomPaint 作为封装,内部维护着 RenderCustomPaint 渲染对象处理通用的逻辑。这样最大的好处是将 RenderObject 相关的操作封装框架中,抽象出 CustomPainter 暴露给用户,提供绘制的接口。这样框架的使用者就无须和 RenderObject 打交道,同时也能通过回调的 Canvas 操作绘制相关方法,总之就是方便了用户使用。

劣势在于:越是底层的东西,可操作性越大,就越灵活,用起来或理解起来就越复杂。反之,越是上层封装的东西,可操作性越小,就越死板,用起来或理解起来就越简单。为了使用方便,必然要有所牺牲,CustomPainter 暴露出的操作十分有限,刷新和尺寸控制没有直接使用 RenderObject 灵活 。

仅停在上层的使用,虽然也不会影响你做出东西。但能从下往上看,便可以知其然,知其所以然。困惑和疑虑能从原理上给出有力的分析,用起来就不会畏首畏尾,出错时也会有一定的应变能力。本文会介绍几个非 CustomPainter 绘制的组件,看看源码中是如何使用 RenderObject 的。


一、ColoredBox 源码分析

1.测试案例

ColoredBox 的作用就是为自己所在的 size 区域填充颜色。Container 组件源码的 color 属性就是集成该组件实现的。

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ColoredBox(
      color: Colors.green,
      child: SizedBox(
        height: 200,
        width: 200,
      ),
    );
  }
}

2. ColoredBox 组件源码

ColoredBox 继承自 SingleChildRenderObjectWidget ,说明它可以有一个 child,并且是属于RenderObjectWidget 一族。使用它完成需要创建和更新 RenderObject 的任务,可以看到这里相关的 RenderObject_RenderColoredBox 。也就是说绘制的任务是在 _RenderColoredBox 中完成的。

class ColoredBox extends SingleChildRenderObjectWidget {
  
  const ColoredBox({@required this.color, Widget child, Key key})
      : assert(color != null),
        super(key: key, child: child);

  final Color color;

  @override
  _RenderColoredBox createRenderObject(BuildContext context) {
    return _RenderColoredBox(color: color);
  }

  @override
  void updateRenderObject(BuildContext context, _RenderColoredBox renderObject) {
    renderObject.color = color;
  }
	//略...
}

3._RenderColoredBox 渲染对象源码

下面就是 _RenderColoredBox 的全部源码。在 paint 方法中,当尺寸大于 Size.zero 时,会使用传入的颜色绘制矩形。渲染对象会形成树状结构,成为 渲染树。 如果_RenderColoredBoxchild 非空,会再绘制子渲染对象。

class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
  _RenderColoredBox({@required Color color})
    : _color = color,
      super(behavior: HitTestBehavior.opaque);

  Color get color => _color;
  Color _color;
  set color(Color value) {
    assert(value != null);
    if (value == _color) {
      return;
    }
    _color = value;
    markNeedsPaint();
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (size > Size.zero) {
      // tag 1: 绘制矩形
      context.canvas.drawRect(offset & size, Paint()..color = color);
    }
    if (child != null) {
      // tag 2 : 孩子非空则绘制孩子
      context.paintChild(child, offset);
    }
  }
}

结合之前 CustomPaint 组件的绘制原理,整体来看是一致的,只不过这里将绘制方法实现了,而 RenderCustomPaint 将绘制逻辑抽象出去,交由用户处理。


二、图片绘制分析
1.Image 组件

Image 组件是一个 StatefulWidget,该类组件,其 State 依赖其他的 Widget 完成构建任务,自身不承担创建渲染对象的任务。如下,_ImageState 主要依赖于 RawImage 进行构建。


RawImage 继承自 LeafRenderObjectWidget,它是一个 RenderObjectWidget ,所以需要完成 创建和维护 RenderObject 的任务。


其中 RawImage#createRenderObject 返回的是 RenderImage ,它是一个 RenderObject 对象,也就是说图片的绘制工作将由该对象完成。


2.RenderImage 渲染对象

RenderImage 是一个 RenderObject 对象,这里只看它的 paint 方法。如下,当 _imagenull 时,会执行 paintImage 方法,将 canvas 及需要的绘制参数传入。

paintImage 方法中,最终还是通过 canvas 绘制图片的相关 API 进行操作的。所以我们传入 Image 组件中的参数都可以在 RenderImage 中找到其使用的场景和作用。


三、文字绘制分析

1.Text 组件

Image 组件是一个 StatelessWidget,该类组件,依赖其他的 Widget 完成构建任务。自身不承担创建渲染对象的任务。如下,Text 主要依赖于 RichText 进行构建。


RichText 继承自 MultiChildRenderObjectWidget ,是一个 RenderObject 对象。所以需要完成 创建和维护 RenderObject 的任务。


其中 RichText#createRenderObject 返回的是 RenderParagraph ,它是一个 RenderObject 对象,也就是说文字的绘制工作将由该对象完成。


2.RenderParagraph 渲染对象

RenderParagraph 是一个 RenderObject 对象,这里只看它的 paint 方法。如下,文字的绘制通过 —textPainter 成员完成。


TextPaint#paint 通过 canvas.drawParagraph 完成文字的绘制。

可以看出,组件并不是自身来完成绘制工作,而是通过对应的 RenderObject 进行绘制。要知道 RenderObject 除了绘制之外,还有一个重要的任务,就是 布局。 所以 Flutter 框架内 RenderObject 是一个非常重要的类。和 Widget 不同,一个 RenderObject 的生命较长,在重新构建时,只是更新了 Widget 对象,并用新的 Widget 提供的信息对 RenderObject 进行 更新。这也是 Flutter 框架一个非常好的处理。关于这点在 Flutter 绘制探索 4 | 深入分析 setState 重建和更新 里有详细论述。


四、再看 Widget - Element - RenderObject

1. Widget 组件

Widget 是我们最先接触的对象,它最大的特点是 所以的成员属性需要是 final 。这表示:该族对象,一旦实例化就无法修改自身配置属性。需要新的值时,会被重新创建含有新新配置信息的对象。是炮灰般的存在。按照继承关系,大致可以分为如下五种:

[1]. StatelessWidget 不可变状态组件
[2]. StatefulWidget 可变状态组件
[3]. ProxyWidget 代理组件
[4]. PreferredSizeWidget 固定尺寸组件
[5]. RenderObjectWidget 渲染对象组件

其实我更想把 Widget 分为两种 组合型 Widget渲染型 Widget。上面的前四种 Widget 都是使用已有的 Widget 进行组合,并不承担维护 RenderObject 的任务。


2.Element 元素

Flutter 框架中,Widget 主要用途就是创建 Element 。元素分为两大类 ComponentElement 组合型元素 和 RenderObjectElement 渲染型元素。在根 Element#mount 会通过依次触发子元素的 mount,从而形成一个元素树结构。

RenderObjectElement 会持有 RenderObject 成员,该成员的创建是由 RenderObjectWidget 完成的。这句话,就是 ElementRenderObjectWidget 三者最重要的关系。而 ComponentElement 不持有 RenderObject ,所以和渲染对象是无关的,只是像它的名字那样进行 Component (组合)。


3.RenderObject 渲染对象

RenderObjectRenderObjectElement 持有,被 RenderObjectWidget 创建。它的实例化时机在 RenderObjectElement#mount 时,创建后会进行 attach 关联到 渲染树 中,从跟依次形成渲染树。

RenderObject 负责 绘制 paint布局 layout,是最核心的对象。无论是可视的组件,还是用于布局的组件,它们功能实现都依赖于 RenderObject。我们如果能力足够,也可以效仿源码中的处理,自己实现 RenderObject 进行绘制、布局。


到这里,本系列就结束了。七天日更,七日打卡。两年前,第一次接触 Flutter ,也是 七天的日更 ,不过当初Flutter 的知识相关的非常少,层次较低,不规范的使用也在所难。随着对 Flutter 认知的加深,也希望在此通过自己的分享,来展现一个更真实的 Flutter 世界。通过这七篇文章,希望能让你对 Flutter 有一个更全面的认识,特别是在绘制方面,通过了解内在实现,从而做到游刃有余。下面对七篇做一个特写:


@张风捷特烈 2021.01.17 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~