Flutter 必知必会系列 —— 有迹可循的 Render 布局过程

618 阅读6分钟

这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

往期精彩

👉  Flutter 必知必会系列——三颗树到底是什么

👉  Flutter 必知必会系列—— Element 的更新复用机制

👉  Flutter 必知必会系列 —— Element 更新实战

👉  Flutter 必知必会系列 —— Render 树的布局绘制

👉  深入理解Flutter布局约束

之前的文章中,我们知道了 Widget、Element 和 RenderObject 的分工,以及 RenderObject 布局的理论。尤其是 👉 深入理解Flutter布局约束 总结的:向下👇传递约束。向上👆传递尺寸

理论就要从客观实际中抽出来,又在客观实际中充分证明。
这一篇文章我们就从代码的角度,验证 👉  深入理解Flutter布局约束 中的两个小例子。


案例分析

案例一:最小还是最大

案例代码就是 深入理解Flutter布局约束的案例一


void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RedContainer();
  }
}

class RedContainer extends StatelessWidget {
  const RedContainer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
    );
  }
}

我们在页面上直接返回了红色背景的 Container。没有设置宽高。大家已经知道了会整个屏幕都是红色,而不是啥也不显示。
理由呢就是:

屏幕是 Container 的父节点,屏幕强制 Container 的大小和screen相同。
因此, Container 使用红色充满了整个屏幕。

下面我们就代码的角度,来看整个过程。

我们是在屏幕上直接显示了 RedContainer ,相当于 Flutter 的根节点的直接子节点就是 RedContainer,所以 红色 Container 的盒子约束就是:

const BoxConstraints({
  this.minWidth = 屏幕宽度,
  this.maxWidth = 屏幕宽度,
  this.minHeight = 屏幕高度,
  this.maxHeight = 屏幕高度,
});

虽然代码写的是 Container( color: Colors.red, ) ,但是实际构造的是 ColoredBox

企业微信截图_e929b971-1df1-44dd-af42-83b0a3e66206.png

构造的节点如下:

image.png

ColoredBox 设置了红色背景,LimitedBox 和 ConstrainedBox 设置了布局宽高。

约束是层层传递的,我们从 ColoredBox 到 ConstrainedBox 依次追下来。

下面就是 ColoredBox 对应的 _RenderColoredBox 的布局过程:

@override
void performLayout() {
  if (child != null) {
    child!.layout(constraints, parentUsesSize: true);
    size = child!.size;
  } else {
    size = computeSizeForNoChild(constraints);
  }
}

我们看到 ColoredBox 没参与布局过程,并且红色区域的宽高还是子节点 LimitedBox 的宽高。

布局的时候直接调用了子节点的布局过程,约束还是从父节点透传的。

这里入参的 constraints 就是页面约束屏幕宽高

minWidth:就是屏幕宽度
maxWidth:constraints.hasBoundedWidth是true,所以也是屏幕宽度
minHeight:就是屏幕高度
maxHeight:和最大宽度相似,是屏幕高度/

下面我们在追 LimitedBox 的布局过程。构造的 LimitedBox 如下:


if (child == null && (constraints == null || !constraints.isTight)) {
  current = LimitedBox(
    maxWidth: 0.0,
    maxHeight: 0.0,
    child: ConstrainedBox(constraints: const BoxConstraints.expand()),
  );
}

LimitedBox 的最大宽度 和 最大高度都是 0,子节点是 ConstrainedBox

LimitedBox 创建的 RenderBox 是:RenderLimitedBox

image.png

下面,我们看 RenderLimitedBox 的布局过程(performLayout):

@override
void performLayout() {
  size = _computeSize(
    constraints: constraints,
    layoutChild: ChildLayoutHelper.layoutChild,
  );
}

Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild }) {
  if (child != null) {
    final Size childSize = layoutChild(child!, _limitConstraints(constraints));
    return constraints.constrain(childSize);
  }
  return _limitConstraints(constraints).constrain(Size.zero);
}

我们看到通过 _computeSize 确定了尺寸,_computeSize 方法的 constraints 约束就是 ColoredBox 传进来的约束 ———— 屏幕宽高。
在实际的 _computeSize 方法中,我们走到了 child != null 的判断。

image.png

虽然 Container 的 child 是 null,但是构造的 LimitedBox 是有子节点的。
这里我们看到,它对原始的 constraints 进行了限制计算(_limitConstraints)。并将计算的结果,作为子节点的约束。

重点就是计算:

BoxConstraints _limitConstraints(BoxConstraints constraints) {
  return BoxConstraints(
    minWidth: constraints.minWidth,
    maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth),
    minHeight: constraints.minHeight,
    maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight),
  );
}

这里的入参 constraints 还记得是谁么,就是 ColoredBox 的约束 —— 屏幕宽高

生成的值如下: minWidth:就是屏幕宽度
maxWidth:constraints.hasBoundedWidth 是 true,所以也是屏幕宽度
minHeight:就是屏幕高度
maxHeight:和最大宽度相似,是屏幕高度/

也就是说,生成的子节点约束就是屏幕宽高

image.png

同样的道理,我们再追到 RenderConstrainedBox 中看布局测量。

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  if (child != null) {
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}

同样,这里的 constraints 就是父节点传进来的约束 ———— 屏幕宽高。
RenderConstrainedBox 下面没有子节点。
所以尺寸就是 _additionalConstraints.enforce(constraints).constrain(Size.zero) 的结果。

_additionalConstraints 是谁呢?就是 Container 中 build 传递的。

image.png

_additionalConstraints 如下:

const BoxConstraints({
  this.minWidth = double.infinity,
  this.maxWidth = double.infinity,
  this.minHeight = double.infinity,
  this.maxHeight = double.infinity,
});

enforce 计算过程如下:

BoxConstraints enforce(BoxConstraints constraints) {
  return BoxConstraints(
    minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
    maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
    minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
    maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
  );
}

大家可能不熟悉 clamp 方法,clamp 是一个数学方法:在一个合理的范围取值,要么是数值本身,要么是两端。

如果数值,在两端之间,那 clamp 的结果就是数值本身。如果小于左断点,那就是左端点的值,如果大于右端点,那就是右端点的值。这就是夹计算

所以 enforce 的结果是:

const BoxConstraints({
  this.minWidth = 屏幕宽度,
  this.maxWidth = 屏幕宽度,
  this.minHeight = 屏幕高度,
  this.maxHeight = 屏幕高度,
});

我们在看 constrain 计算过程:

Size constrain(Size size) {
  Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
  return result;
}

double constrainWidth([ double width = double.infinity ]) {
  return width.clamp(minWidth, maxWidth);
}

宽度是 屏幕宽度.clamp(0,无穷)
高度是 屏幕高度.clamp(0,无穷)

到此就确定了尺寸,屏幕宽度。并且将尺寸层层上传。
所以 整个屏幕显示红色的屏幕

整个过程就验证了:约束向下传递,尺寸向上传递

案例二:能够显示指定尺寸吗

我们现在改变一下:指定一下宽高

class RedContainer extends StatelessWidget {
  const RedContainer({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      width:100,
      height:100,
    );
  }
}

显示的也是整个屏幕,因为 父节点强制显示整个屏幕,下面我们就看强制的过程。

这里我们先看一下 Container 构造方法:

Container({
  Key? key,
  this.alignment,
  this.padding,
  this.color,
  this.decoration,
  this.foregroundDecoration,
  double? width,
  double? height,
  BoxConstraints? constraints,
  this.margin,
  this.transform,
  this.transformAlignment,
  this.child,
  this.clipBehavior = Clip.none,
}) : constraints =
      (width != null || height != null)
        ? constraints?.tighten(width: width, height: height)
          ?? BoxConstraints.tightFor(width: width, height: height)
        : constraints,
     super(key: key);
     

因为我们设置了 width 和 height,所以就会在构造方法中 tightFor Constraints 。构造的结果是:

const BoxConstraints({
  this.minWidth = 100,
  this.maxWidth = 100,
  this.minHeight = 100,
  this.maxHeight = 100,
});

根据 build 方法的逻辑,就会构造出以下节点

image.png

ColorBox 我们就不说了,重点是 ConstrainedBox

image.png

我们就看对应的 RenderConstrainedBox 的布局逻辑。

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  if (child != null) {
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}

_additionalConstraints 就是 100 * 100。
constraints 就是父节点传递的约束 屏幕宽高。
enforce 的结果依然是屏幕宽高。

BoxConstraints enforce(BoxConstraints constraints) {
  return BoxConstraints(
    minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
    maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
    minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
    maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
  );
}

minWidth 是 100
constraints.minWidth 是 屏幕宽度(大于100)
constraints.maxWidth 是 屏幕宽度(大于100)

所以结果的 minWidth 是 屏幕宽度,其他的同理。因此,布局 ColorBox 的约束就是屏幕宽高。

有了案例一的铺垫,显示的结果还是整个屏幕就是红色。

整个过程就验证了:父节点强制子节点是多大的。就是文档中的一句话 When a widget tells its child that it must be of a certain size, we say the widget supplies tight constraints to its child。

总结

image.png

上面我们通过两个个案例,来一步一步追了布局的流程,知道了怎么追一个组件的布局。结论不重要,知道怎么追才是最重要的。

大家只需要按着下面的步骤就可以:

第一:通过Widget,找到渲染对象

第二:找到渲染对象的 performLayout 和 performResize