Flutter RenderBox指南——绘制篇

本文基于1.12.13+hotfix.8版本源码分析。

0、大纲

  1. RenderBox的用法
  2. 通过RenderObjectWidget把RenderBox塞进界面

1、RenderBox

在flutter中,我们最常接触的,莫过于各种各样的widget了,但是,实际负责渲染的RenderObject是很少接触的(它们之间的关联可以看看闲鱼的这篇文章://www.yuque.com/xytech/flutter/tge705)。而作为一名天天向上的程序员,我们自然要去学习一下它的原理,做到知其然且知其所以然。本文会先来看看RenderBox的用法,以此抛砖引玉,便于后面继续深入flutter的绘制原理。

首先,RenderBox是RenderObject的子类,它将坐标系转换成我们熟知的笛卡尔坐标系,更便于使用。使用RenderBox进行绘制(本文暂时不讲child),我们需要做三件事:

(1)测量

第一步,我们需要确定视图大小,并赋值给父类的size属性。测量有两种情况,第一种是size由自身决定,第二种是由parent决定。

首先,由自身决定size的情况,需要在performLayout方法中完成测量,通过父类的constraints可得到满足约束的值:

  @override
  void performLayout() {
    size = Size(
      constraints.constrainWidth(200),
      constraints.constrainHeight(200),
    );
  }

第二种情况,size由parent决定,这种情况下视图大小应该完全通过parent提供的constraints测量,不存在其它因素。这种情况下,只要parent的约束不发生变化,就不会重新测量。

这种情况需要重写sizedByParent并返回true,然后在performResize中完成测量,并且performLayout中不能对size赋值! 感兴趣的同学也可以两边都写,看看会发生什么:)。

  @override
  void performLayout() {}
  
  @override
  void performResize() {
    size = Size(
      constraints.constrainWidth(200),
      constraints.constrainHeight(200),
    );
  }

  @override
  bool get sizedByParent => true;
(2)绘制

RenderBox的绘制与android原生的view绘制非常相似,同样是Paint+Canvas的组合,而且api也非常接近,会非常容易上手。

  @override
  void paint(PaintingContext context, Offset offset) {
    Paint paint = Paint()
      ..color = _color
      ..style = PaintingStyle.fill;
    context.canvas.drawRect(
        Rect.fromLTRB(
          0,
          0,
          size.width,
          size.height,
        ),
        paint);
  }

这样是不是就万事大吉了呢?如果通过上面的代码进行绘制,你会发现,不管在外层怎么设置位置,绘制出来的矩形都是固定在屏幕左上角的!怎么回事?

这里就是flutter中绘制与android的最大不同:在这里绘制的坐标系是全局坐标系,即原点在屏幕左上角,而非视图左上角。

细心的同学可能已经发现,paint方法中还有一个offset参数,这就是经过parent的约束后,当前视图的偏移量,绘制时应该将它考虑进去:

  @override
  void paint(PaintingContext context, Offset offset) {
    Paint paint = Paint()
      ..color = _color
      ..style = PaintingStyle.fill;
    context.canvas.drawRect(
        Rect.fromLTRB(
          offset.dx,
          offset.dy,
          offset.dx + size.width,
          offset.dy + size.height,
        ),
        paint);
  }
(3)更新

在flutter中,是由Widget的配置发生变更而引起的rebuild,而这就是我们要实现的第三步:当视图属性发生变更时,标记重新布局或重新绘制,当屏幕刷新时就会做相应的刷新。

这里涉及到两个方法:markNeedsLayout、markNeedsPaint。顾名思义,前者标记重布局,后者标记重绘。

我们需要做的,就是根据属性的影响范围,在更新属性时,调用合适的标记方法,例如color变化时调用markNeedsPaint,width变化时调用markNeedsLayout。另外,两者都需要更新的情况下,调用markNeedsLayout即可,不需要两个方法都调。

  set width(double width) {
    if (width != _width) {
      _width = width;
      markNeedsLayout();
    }
  }

  set color(Color color) {
    if (color != _color) {
      _color = color;
      markNeedsPaint();
    }
  }

2、RenderObjectWidget

(1)简介

上面讲了一大堆RenderBox的用法,但是,这玩意儿怎么用到我们熟知的Widget里面去?

按照正常流程,我们得实现一个Element和一个Widget,然后在Widget中创建Element,在Element中创建和更新RenderObject,另外还得管理一大堆状态,处理非常繁琐。所幸flutter为我们封装了这一套逻辑,即RenderObjectWidget。

相信对flutter有所了解的同学都对StatelessWidget和StatefulWidget不陌生,但其实,StatelessWidget和StatefulWidget仅负责属性、生命周期等的管理,在它们的build方法实现中都会创建RenderObjectWidget,通过它来实现与RenderObject的关联。

举个栗子,我们经常使用的Image是个StatefulWidget,对应的state的build方法中实际返回了一个RawImage对象,而这个RawImage是继承自LeafRenderObjectWidget的,这正是RenderObjectWidget的一个子类;再比如Text,它build方法中创建的RichText是继承自MultiChildRenderObjectWidget,这同样是RenderObjectWidget的一个子类。

我们再看看RenderObjectWidget顶部的注释即可明白:

RenderObjectWidgets provide the configuration for [RenderObjectElement]s,
which wrap [RenderObject]s, which provide the actual rendering of the
application.

大概意思就是RenderObject才是实际负责渲染应用的,而RenderObjectWidget提供包装了RenderObject的配置,方便我们使用。

另外,flutter还分别实现了几个子类,进一步封装了RenderObjectWidget,它们分别是LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget。其中,LeafRenderObjectWidget是叶节点,不含子Widget;SingleChildRenderObjectWidget仅有一个child;而MultiChildRenderObjectWidget则是含有children列表。这几个子类根据child的情况分别创建了对应的Element,所以通过这几个子类,我们只需要关注RenderObject的创建和更新。

(2)用法

以最简单的LeafRenderObjectWidget为例,我们需要实现createRenderObject、updateRenderObject两个方法:

  class CustomRenderWidget extends LeafRenderObjectWidget {
  CustomRenderWidget({
    this.width = 0,
    this.height = 0,
    this.color,
  });

  final double width;
  final double height;
  final Color color;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return CustomRenderBox(width, height, color);
  }

  @override
  void updateRenderObject(BuildContext context, RenderObject renderObject) {
    CustomRenderBox renderBox = renderObject as CustomRenderBox;
    renderBox
      ..width = width
      ..height = height
      ..color = color;
  }
}