Flutter 笔记 | Flutter 核心原理(三)布局(Layout )过程

布局过程

Layout(布局)过程主要是确定每一个组件的布局信息(大小和位置),Flutter 的布局过程如下:

  1. 父节点向子节点传递约束(constraints)信息,限制子节点的最大和最小宽高。
  2. 子节点根据约束信息确定自己的大小(size)。
  3. 父节点根据特定布局规则(不同布局组件会有不同的布局算法)确定每一个子节点在父节点布局空间中的位置,用偏移 offset 表示。
  4. 递归整个过程,确定出每一个节点的大小和位置。

可以看到,组件的大小是由自身决定的,而组件的位置是由父组件决定的。

下面是官网的一张图,它用三句话描述了 Flutter 布局过程的精髓:

在这里插入图片描述

Flutter 中的布局类组件很多,根据孩子数量可以分为单子组件和多子组件,下面我们先通过分别自定义一个单子组件和多子组件来直观理解一下Flutter的布局过程,之后会介绍一下布局更新过程和 Flutter 中的 Constraints(约束)。

单子组件布局示例

我们实现一个单子组件 CustomCenter,功能基本和 Center 组件对齐,通过这个实例我们演示一下布局的主要流程。

首先,我们定义组件,为了介绍布局原理,我们不采用组合的方式来实现组件,而是直接通过定制 RenderObject 的方式来实现。因为居中组件需要包含一个子节点,所以我们直接继承 SingleChildRenderObjectWidget

class CustomCenter extends SingleChildRenderObjectWidget {
   
  const CustomCenter2({
   Key? key, required Widget child})
      : super(key: key, child: child);

  
  RenderObject createRenderObject(BuildContext context) {
   
    return RenderCustomCenter();
  }
}

接着实现 RenderCustomCenter。这里直接继承 RenderObject 会更接近底层一点,但这需要我们自己手动实现一些和布局无关的东西,比如事件分发等逻辑。为了更聚焦布局本身,我们选择继承自RenderShiftedBox,它是RenderBox的子类(RenderBox继承自RenderObject),它会帮我们实现布局之外的一些功能,这样我们只需要重写performLayout,在该函数中实现子节点居中算法即可。

class RenderCustomCenter extends RenderShiftedBox {
   
  RenderCustomCenter({
   RenderBox? child}) : super(child);

  
  void performLayout() {
   
    //1. 先对子组件进行layout,随后获取它的size
    child!.layout(
      constraints.loosen(), //将约束传递给子节点
      parentUsesSize: true, // 因为我们接下来要使用child的size,所以不能为false
    );
    //2.根据子组件的大小确定自身的大小
    size = constraints.constrain(Size(
      constraints.maxWidth == double.infinity
          ? child!.size.width
          : double.infinity,
      constraints.maxHeight == double.infinity
          ? child!.size.height
          : double.infinity,
    ));

    // 3. 根据父节点子节点的大小,算出子节点在父节点中居中之后的偏移,然后将这个偏移保存在
    // 子节点的parentData中,在后续的绘制阶段,会用到。
    BoxParentData parentData = child!.parentData as BoxParentData;
    parentData.offset = ((size - child!.size) as Offset) / 2;
  }
}

布局过程请参考注释,在此需要额外说明有3点:

  1. 在对子节点进行布局时, constraintsCustomCenter 的父组件传递给自己的约束信息,我们传递给子节点的约束信息是constraints.loosen(),下面看一下loosen的实现源码:
BoxConstraints loosen() {
   
  return BoxConstraints(
    minWidth: 0.0,
    maxWidth: maxWidth,
    minHeight: 0.0,
    maxHeight: maxHeight,
  );
}

很明显,CustomCenter 约束子节点最大宽高不超过自身的最大宽高。

  1. 子节点在父节点(CustomCenter)的约束下,确定自己的宽高;此时CustomCenter会根据子节点的宽高确定自己的宽高,上面代码的逻辑是,如果CustomCenter父节点传递给它最大宽高约束是无限大时,它的宽高会设置为它子节点的宽高。注意,如果这时将CustomCenter的宽高也设置为无限大就会有问题,因为在一个无限大的范围内自己的宽高也是无限大的话,那么实际上的宽高到底是多大,它的父节点会懵逼的!屏幕的大小是固定的,这显然不合理。如果CustomCenter父节点传递给它的最大宽高约束不是无限大,那么是可以指定自己的宽高为无限大的,因为在一个有限的空间内,子节点如果说自己无限大,那么最大也就是父节点的大小。所以,简而言之,CustomCenter 会尽可能让自己填满父元素的空间。

  2. CustomCenter 确定了自己的大小和子节点大小之后就可以确定子节点的位置了,根据居中算法,将子节点的原点坐标算出后保存在子节点的 parentData 中,在后续的绘制阶段会用到,具体怎么用,我们看一下RenderShiftedBox中默认的 paint 实现:


void paint(PaintingContext context, Offset offset) {
   
  if (child != null) {
   
    final BoxParentData childParentData = child!.parentData! as BoxParentData;
    //从child.parentData中取出子节点相对当前节点的偏移,加上当前节点在屏幕中的偏移,
    //便是子节点在屏幕中的偏移。
    context.paintChild(child!, childParentData.offset + offset);
  }
}
performLayout 流程

可以看到,布局的逻辑是在 performLayout 方法中实现的。我们梳理一下 performLayout 中具体做的事:

  1. 如果有子组件,则对子组件进行递归布局。
  2. 确定当前组件的大小(size),通常会依赖子组件的大小。
  3. 确定子组件在当前组件中的起始偏移。

在Flutter组件库中,有一些常用的单子组件比如 Align、SizedBox、DecoratedBox 等,都可以打开源码去看看其实现。

下面我们看一个多子组件的例子。

多子组件布局示例

实际开发中我们会经常用到贴边左-右布局,现在我们就来实现一个 LeftRightBox 组件来实现左-右布局,因为LeftRightBox 有两个孩子,用一个 Widget 数组来保存子组件。

首先我们定义组件,与单子组件不同的是多子组件需要继承自 MultiChildRenderObjectWidget

lass LeftRightBox extends MultiChildRenderObjectWidget {
   
  LeftRightBox({
   
    Key? key,
    required List<Widget> children,
  })  : assert(children.length == 2, "只能传两个children"),
        super(key: key, children: children);

  
  RenderObject createRenderObject(BuildContext context) {
   
    return RenderLeftRight();
  }
}

接下来需要实现 RenderLeftRight,在其 performLayout 中我们实现实现左-右布局算法:

class LeftRightParentData extends ContainerBoxParentData<RenderBox> {
   }

class RenderLeftRight extends RenderBox
    with
        ContainerRenderObjectMixin<RenderBox, LeftRightParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, LeftRightParentData> {
   
 
  // 初始化每一个child的parentData        
  
  void setupParentData(RenderBox child) {
   
    if (child.parentData is! LeftRightParentData)
      child.parentData = LeftRightParentData();
  }

  
  void performLayout() {
   
    final BoxConstraints constraints = this.constraints;
    RenderBox leftChild = firstChild!;
    
    LeftRightParentData childParentData =
        leftChild.parentData! as LeftRightParentData;
    
    RenderBox rightChild = childParentData.nextSibling!;

    //我们限制右孩子宽度不超过总宽度一半
    rightChild.layout(
      constraints.copyWith(maxWidth: constraints.maxWidth / 2),
      parentUsesSize
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值