阅读 1942

花两天时间做了15个例子的解析,彻底掌握Flutter的布局原理

我用两天时间做了15个例子的解析,让你彻底掌握Container,Padding,Row,Center等组件与Flutter的布局原理

学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概二十篇左右文章分析,欢迎关注,共同进阶Flutter。

往期文章

1、为什么不建议大家使用setState()。

2、面试官问我State的生命周期,该怎么回答

3、Flutter的布局约束原理

导语

PS:由于本文例子较多,建议可以先看样例,如果符合心理预期且理解原理可以直接跳过,如果不符合心理预期再看解释即可。理解本文后你将彻底明白常用的Padding,Container,Row,Cloumn,Center,Align,Expand等组件的布局行为。

上一期文章 总结了30个例子之后,我悟到了Flutter的布局原理点赞已破50(QAQ)本期含泪整理了我认为最有代表性的15个例子,由于例子较多,整体上我先分为了ContainerAlignFlex其他四大类,而每个例子我基本会以 案例+渲染树+文字说明 整个流程,好了下面直接开始进入正题!!!(如果没看过上期文章的建议一定先看下,明白Flutter的整体渲染流程。本文周末整理了两天,求点赞、求关注QAQ)

所有Case均来自于Flutter.cn提供的深入理解 Flutter 布局约束一文。


Container类布局说明

通过上期文章我们知道Flutter的layout() 职能主要是计算控件自身的尺寸和位置偏移 ,整个布局过程就是向下约束 向上传值的过程。如果你对以下内容有疑惑,强烈建议先看看总结了30个例子之后,我悟到了Flutter的布局原理

Case 1:直接返回Container

渲染树树如下

上期文章提到这这种情况下,Container会在渲染树上生成三个节点,页面会传给子节点一个撑满屏幕的紧约束,这个约束向下传递。最后到ContainerBox的时候收到这个约束,通过_additionalConstraints.enforce(constraints)计算自身大小,这个方法会根据自身的属性与接受来的约束决定大小,计算后任然撑满屏幕,这个大小信息不断上传,最后Container布满了屏幕。(如果到这里还不太明白的可以先看看总结了30个例子之后,我悟到了Flutter的布局原理)

Case 2:Container指定宽高

渲染树树如下

对比Case 1其实整个渲染树结构没有变,区别只是为Container加上了宽高限制,这个宽高限制会附加在ContainedBox上。但是由于上面传递下来的约束是撑满屏幕的紧约束,所以计算后任然是撑满屏幕和Case 1一样。

Case 3:Container外层设置约束

你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样,Container任然撑满了整个屏幕。我们还是来看Render树的结构

首先最外层的ConstrainedBox接收到来自页面的紧约束(max=min=屏幕宽度),计算采用上面的计算方式还是得到了一个同样的约束,之后这个约束向下传递,到最后任然和Case 1一样,这个例子把最外层的ConstrainedBox改成Container,渲染树的结构和最终结果也一样。


Align类布局说明

Aling组件我们一般会在需要控制child控件的相对位置时候使用,例如居中,或者顶部。我们常用的Center组件就是继承于它,根据上期的提到的方法,我们先查看Align组件的布局规则。Align组件对应的RenderObject对象是RenderPositionedBox其performLayout()如下

  @override
  void performLayout() {
  	//缩放因子,这里一般为null,这里我们不关注
    final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
    final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
    if (child != null) {
      //使用constraints.loosen()模式测量子节点,并且标志parentUsesSize位true
      child.layout(constraints.loosen(), parentUsesSize: true);
      //根据子节点的大小与缩放因子计算自生的宽高
      size = constraints.constrain(Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.infinity,
                                            shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.infinity));
      //根据alignment确定子节点的位置
      alignChild();
    } else {
      size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
                                            shrinkWrapHeight ? 0.0 : double.infinity));
    }
  }
   //生成一个松约束,范围为0到max
   BoxConstraints loosen() {
    return BoxConstraints(
      minWidth: 0.0,
      maxWidth: maxWidth,
      minHeight: 0.0,
      maxHeight: maxHeight,
    );
  }
复制代码

Align组件里有一些缩放相关的规则,可以忽略。这里我们主要关注里面测量的部分,根据代码这个布局过程其实分三步

  • 1、通过使用constraints.loosen()模式测量子节点,并且标志parentUsesSize位true
  • 2、根据子节点的大小与缩放因子计算自生的宽高
  • 3、根据alignment确定子节点的位置 在第一步中,RenderPositionedBox将上级传递的约束变成了松约束范围为0到max,将这个松约束传递给子节点进行测量大小。最后根据alignment确定子节点的位置。下面我们看看实际的例子

Case 4:Center下面放指定宽高的Container

渲染树树如下

RenderPositionedBox将上级传递的约束变成了将页面传来的紧约束变成了松约束,一直传递到了ConstrainedBox中,而它的performLayout会执行:

size = _additionalConstraints.enforce(constraints).constrain(Size.zero);

//以自身约束_additionalConstraints为主,同时尊重给定的约束
  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),
    );
  }
复制代码

ConstrainedBox上的_additionalConstraints是一个宽高都为100的紧约束,最后参考父节点传来的松约束(0-屏幕宽度)算出自己宽高任然为100,之后这个尺存向上传递。Center计算自己高度的时候任然为屏幕宽高(Center撑满了屏幕,想想为什么会是这样),最后根据alignment=Alignment.center算出了hild的位置。

  @protected
  void alignChild() {
    final BoxParentData childParentData = child.parentData;
    //通过child的parentData属性,告诉child的应该放在哪个位置
    childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
  }
复制代码

Case 5:Align下面放指定宽高为100的Container

这个案例其实和上面的一样,因为Center本身就是继承自Align。只不过这个case中,Container被放在了右下角。提这个例子是想回应下上一个例子中提到的在计算最后Center撑满了屏幕。因为只有Align这个组件比child大的时候,设置alignment才有意义(可以直接看RenderPositionedBox的计算规则)。

Case 6: Center下放一个宽高无限的Container

渲染树树如下

结构和上面没有变化,不过在ConstrainedBox计算自身宽高的时候,由于这时_additionalConstraintsdouble.infinity所以计算后的宽高是撑满屏幕。其实到这里大家不难发现,ConstrainedBox的宽高计算关键在于理解_additionalConstraints.enforce(constraints)这个方法:根据自身的约束同时尊重父节点的约束。所以,如果这如果我们指定Container宽高为0,则Container会消失。但如果我们不指定width和height会发生什么呢。下个例子看看。

Case 7:Center下放一个不指定宽高的Container

可能这个时候你觉得Container会消失,但并不这样的,不指定Container的宽高和将其设置为0是不一样的,这里渲染树和上面任然保持一致,所以就不贴图了。而变成和屏幕一样大小的关键在于Container组件的布局行为,查看Container的build方法:

发现Container在不设置约束,并且没有子节点的时候,会给自己添加一个宽高都等于double.infinity的紧约束,所以最后撑满了屏幕。

Case 8:Center下放一个具有Padding的Container下面再放一个Container

有了上面的基础,这个例子很容易明白。但因为我们设置了padding值,所以整个树的结构也发生了改变,这个例子主要分析一下padding的布局行为,我们先看渲染树

这是由于在Center下面增加了一个带有color和padding属性的Container,所以整个结构中增加了两层,其他的组件的约束规则我们都很熟悉了,这里看下RenderPadding的performLayout:

 @override
  void performLayout() {
    _resolve();
    if (child == null) {
      //如果子节点为空,则自己的宽高由paddign属性决定
      size = constraints.constrain(Size(
        _resolvedPadding.left + _resolvedPadding.right,
        _resolvedPadding.top + _resolvedPadding.bottom,
      ));
      return;
    }
    //返回一个小于_resolvedPadding的松约束
    final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);
    child.layout(innerConstraints, parentUsesSize: true);
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);
    //自身的高度由子节点与padding相加
    size = constraints.constrain(Size(
      _resolvedPadding.left + child.size.width + _resolvedPadding.right,
      _resolvedPadding.top + child.size.height + _resolvedPadding.bottom,
    ));
  }
复制代码

我们看到在测量child的时候使用的是constraints.deflate(_resolvedPadding)这个约束。根据源码上的注释可知,这个结果由当前的约束减去padding得到。所以在case中,RenderPadding向下传递的是一个范围为(0-屏幕宽度减padding)的松约束。而后面在子节点计算完宽高了之后,根据子节点的宽高加上padding形成自己的宽高,向上传递。最后显示在屏幕上形成两个居中叠加的Container。 这里大家可以猜想一下,如果去掉Center会是什么样,欢迎评论区告诉我你的答案~

Case 9:Center下增加一个松约束的ConstrainedBox下Container

渲染树如下:

首先Center将屏幕的约束改为0-屏幕宽高的松约束,之后由于ConstrainedBox上添加了一个松约束宽高范围(70-150),之后这个约束向下传递。最后底部的ConstrainedBox收到这个约束,他自己的additionalConstraints属性是一个宽高无穷的紧约束,这样在调用_additionalConstraints.enforce(constraints)之后计算结果取最大约束150。这个尺寸向上传递,最后这个Container的大小变成了150。这里如果我们为Contianer添加的宽高是0,则取最小的约束70。


Flex类(包含Row,Cloumn)

在日常开发中常使用的Row,Cloumn都是Flex的子类,只是Row和Cloumn会分别为Flex的direction属性设置为不同的值。在Column中direction: Axis.vertical,在Row中direction: Axis.horizontal。Flex对应的RenderObject为RenderFlex,其performLayout比较长,可以分为子节点设置了flex属性(即按比例分配)和未设置该属性,我们通过未设置的方式看看这个过程:

 @override
 void performLayout() {
 //遍历子元素
 while(child!=null){
  ///省略设置Flex的时候
  *************
  //如果子节点没有设置flex的时候:
  BoxConstraints innerConstraints;
  		//如果cross轴上alignment等于CrossAxisAlignment.stretch(即填满纵轴,默认不是这种模式)
        if (crossAxisAlignment == CrossAxisAlignment.stretch) {
          switch (_direction) {
          	//如果主轴方向是水平,则垂直方向为高度强制为当前的最大约束值
            case Axis.horizontal:
              innerConstraints = BoxConstraints(minHeight: constraints.maxHeight,
                                                    maxHeight: constraints.maxHeight);
              break;
            //如果主轴是垂直方向,则水平方向宽度强制为当前的最大约束值
            case Axis.vertical:
              innerConstraints = BoxConstraints(minWidth: constraints.maxWidth,
                                                    maxWidth: constraints.maxWidth);
              break;
          }
        } else {
          //如果cross轴上alignment等于CrossAxisAlignment.center(默认),start,end,baseline
          switch (_direction) {
            case Axis.horizontal:
          	//如果主轴方向是水平,则垂直方向高度为松约束范围0-maxHeight
              innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
              break;
          	//如果主轴方向是垂直,则水平方向宽度为为松约束0-maxWidth
            case Axis.vertical:
              innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
              break;
          }
         //测量子布局
        child.layout(innerConstraints, parentUsesSize: true);
        //计算主轴上的尺寸
        allocatedSize += _getMainSize(child);
        //纵轴上的高度取最大的一个
        crossSize = math.max(crossSize, _getCrossSize(child));
  }
复制代码

看起来比较复杂,因为Flex(Cloumn,Row)组件在测量尺寸的过程需要对横轴和纵轴进行计算。首先Flex会遍历每一个子节点,先检查crossAxisAlignment == CrossAxisAlignment.stretch是否成立,这个条件表示对应的Flex组件在纵轴上是否为撑满。以Row为例,如果条件成立则Row会占满垂直方向,Cloumn则会占满水平方向。看代码可知因为这个条件下对应的纵轴约束是一个minWidth和maxWidth都等于constraints.maxWidth的紧约束,所以对子节点的约束在纵轴上是紧约束。不过一般我们使用Flex类的组件的时候,如果不设置都走下面的分支,该流程分4步:

  • 1、根据Flex的方向生成一个松约束innerConstraints
  • 2、测量子布局
  • 3、累加计算主轴的尺寸
  • 4、纵轴上的高度取最大的

以Row为例,在大多数我们使用Flex的时候,对于子节点在水平上是一个0-double.infinity的松约束,即表示Row不会约束子节点的宽度,想要多少都行(可能出现OverFlow)。而垂直方向则是一个0-maxHeight的松约束。还有点儿懵么?看几个例子就明白了

Case 10:Row下面包含两个带Text的Container

简单来说Text对应的RenderObject是RenderParagraph,每个文字会生成一个节点,而RenderParagraph会向其传递一个0-max的松约束,最后RenderParagraph的尺寸就是所有文字的和。这样整个渲染树的结构变成这个样子:

Row的每一个子节点都收到一个宽高为0-屏幕尺寸的约束,之后'Hello!'计算自己的宽度为50,高20。而'Goodbye!'计算自己的宽度为100,高20。DecoratedBox收到这个宽高渲染了对应的颜色。之后RendexFlex将两个节点的宽相加,高取最大,得到了自己的尺寸宽:50+100,高max(20,20)=20。

Case 11:Row下面包含两个带Text的Container(超长)

这个例子和上面的渲染树结构一致,不同的是这个例子中出现了OverFlow。因为前面我们提到了RenderFlex传递给RenderParagraph的是一个0-屏幕的宽约束,这个约束被传递给每一个文字。RenderParagraph的宽度等于文字的和,所以导致了OverFlow。


其他类型如Scaffold,OverflowBox等

好上面就是我们日常中最常用的几个组件的布局规则了,这里我们再看看一些其他类型的布局规则,加深我们的理解

Case 12:Scafflod下套一个Container

这个例子还是比较有意思,我们看现象。整个Container的蓝色在垂直方向占满屏幕,这是因为Cloum(或者Row)中有个mainAxisSize属性,默认为MainAxisSize.max即尽力撑开,所以最后Cloumn撑满了竖直方向

Case 13: OverflowBox下放一个超长的Container

OverflowBox 允许其子容器设置为任意大小,在这种情况下,容器的宽度为 4000 像素,并且太大而无法容纳在 OverflowBox 中,但是 OverflowBox 会全部显示,而不会发出警告。所以如果你想最快速的解决OverFlow类型的错误,有时可以考虑使用这个组件。

Case 14:SizeBox下面放一个Container

SizeBox也是我们经常用到的一个组件,这个组件背后其实对应的就是ConstrainedBox类的组件。不过由于SizeBox有具体的width和height,所以他这里自身的_additionalConstraints属性是一个紧约束。不过在本例中由于他接受到来自页面的一个宽高都为屏幕尺寸的紧约束,所以导致自身的属性失去了作用,和样例3类似。

Case 15:Row下面嵌套Expanded

前面演示Row的时候,我只说了flex为0的情况,这里给大家留个作业,如果是Row下嵌套Expanded的情况下,整个布局的流程又是什么样呢?欢迎大家留言说出你的看法~


总结

通过两期的文章,现在对于使用BoxConstrain协议的组件我们应该可以了然于胸,整个布局行为其实用一句话来概括就是: 父节点传递约束,子节点向上传递尺寸,最后由父节点决定你的位置。 对于约束传递不同的组件会产生不同的行为,例如Route组件中会向子节点传递一个宽高都为屏幕尺寸的紧约束,使页面的直接节点变成撑满模式。而Align类型的组件则是对子Widget传递一个0-max的松约束。ConstrainedBox有点类似安卓上的布局行为,我自己身上有宽高的属性。但我又得结合父节点给我的约束才能决定我自己的大小。


最后

首先写这篇文章真的耗费了大量的精力,例如Container组件,因为他在设置不同属性的情况下会返回不同的树结构。而我担心直接看逻辑可能有和预期不一样,所以选择了打断点的方式查看结构。但是页面上往往出现干扰性的Container,导致确定这个组件要花不少时间,然后就是分析结构画图。不过既然牛已经吹出去了,那还是要干的。整个过程也彻底的让我明白了Flutter的布局约束原理,其实到最后会发现,无论是Native还是Flutter,大都殊途同归。希望这篇文章也能帮助正在看的你更加清楚这背后的原理!!下一期应该会和大家一起学习Flutter engine层的内容,例如Flutter的启动流程boost等框架~ 欢迎关注!! 学习Flutter的路上,一直在进步! thanks~

文章分类
Android
文章标签