Flutter 笔记 | Flutter 核心原理(五)Box 布局模型和 Sliver 布局模型

文章深入分析了Flutter的布局流程,包括Box布局模型中的Align和Flex,以及Sliver布局模型在ListView等列表组件中的应用。Box布局通过约束计算大小和偏移,而Sliver布局则适应于无限滚动列表,通过Viewport和SliverConstraints管理无限内容的布局。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

根据前文我们已经从宏观上得知:Layout流程的本质是父节点向子节点传递自己的布局约束Constraints,子节点计算自身的大小(Size),父节点再根据大小信息计算偏移(Offset)。在二维空间中,根据大小和偏移可以唯一确定子节点的位置。

Flutter中主要存在两种布局约束——BoxSliver,关键类及其关系如图6-1所示。

图6-1 Layout关键类及其关系

图6-1中,BoxConstraintsSliverConstraints分别对应Box布局和Sliver布局模型所需要的约束条件。ParentDataRenderObject所持有的一个字段,用于为父节点提供额外的信息,比如RenderBox通过BoxParentData向父节点暴露自身的偏移值,以用于Layout阶段更新和Paint阶段。Sliver通过SliverGeometry描述自身的Layout结果,相对Box更加复杂。

Box布局模型

Box类型的Constraints布局在移动UI框架中非常普遍,比如AndroidConstraintLayoutiOSAutoLayout都有其影子。Constraints布局的特点是灵活且高效。Flutter中Box布局的原理如图6-2所示。

在这里插入图片描述

下面主要介绍 Box布局模型中最常见的两种布局——AlignFlex。虽然Flutter源码中提供的Box布局组件远不止这两种,但万变不离其宗,只要深刻理解了BoxConstraints的本质,相信其他布局也不在话下。

Align布局流程分析

本节将分析Box布局中比较有代表性的Align布局,其关键类如图6-3所示。 了解了Align的布局原理之后,相信对于其他关联的Widget也能够触类旁通。

在这里插入图片描述

图6-3中,RenderShiftedBox表示一个可以对子节点在自身中的位置进行控制的单子节点容器,最常见的就是PaddingAlign,其他Widget可自行研究,每个Widget都对应一个实现自身布局规则的RenderObject子类,在此不再赘述。

下面正式分析Align的布局流程。Align对应的RenderObjectRenderPositionedBox,其performLayout方法如代码清单6-1所示。

// 代码清单6-1 flutter/packages/flutter/lib/src/rendering/shifted_box.dart
void performLayout() {
   
   
  final BoxConstraints constraints = this.constraints;
  final bool shrinkWrapWidth = 
// 即使约束为infinity也要处理,使之变成有限长度,否则边界无法确定
      _widthFactor != null || constraints.maxWidth == double.infinity;
  final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight 
     == double.infinity;
  if (child != null) {
   
    // 存在子节点
    child!.layout(constraints.loosen(), parentUsesSize: true); // 布局子节点
    size = constraints.constrain(Size(  // 开始布局自身,见代码清单6-2
    shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
    shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.
        infinity));
    alignChild(); // 计算子节点的偏移,见代码清单6-3
  } else {
   
    // 没有子节点时,一般大小为0,因为最大约束为infinity时shrinkWrapWidth为true
    size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
                                 shrinkWrapHeight ? 0.0 : double.infinity));
  }
}

以上逻辑中,shrinkWrapWidth表示当前宽度是否需要折叠(Shrink),当_widthFactor被设置或者未对子Widget做宽度约束时需要,当子Widget存在时,其大小计算过程如代码清单6-2所示。计算完大小后会调用alignChild方法完成子Widget位置的计算。如果子Widget不存在,则大小默认为0

// 代码清单6-2 flutter/packages/flutter/lib/src/rendering/box.dart
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); // 返回约束内最接近自身的值
}
double constrainHeight([ double height = double.infinity ]) {
   
   
  return height.clamp(minHeight, maxHeight);
}

以上逻辑的核心在于clamp方法,以constrainWidth方法为例,其返回值为minWidthmaxWidth之间最接近width的值。以a.clamp(b, c)为例,将先计算a、b的较大值x,再计算x、c的较小值,并作为最终的结果。下面分析子节点偏移值的计算,如代码清单6-3所示。

// 代码清单6-3 flutter/packages/flutter/lib/src/rendering/shifted_box.dart

void alignChild() {
   
   
  _resolve(); // 计算子节点的坐标
  final BoxParentData childParentData = child!.parentData! as BoxParentData; 
// 存储位置信息
  childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as 
      Offset); // 偏移值
}
void _resolve() {
   
   
  if (_resolvedAlignment != null) return;
  _resolvedAlignment = alignment.resolve(textDirection);
}

以上逻辑首先会将Alignment解析成_resolvedAlignment,其关系如图6-4所示。

图6-4 Alignment关键类

图6-4中,RenderPositionedBoxAlign对应的RenderObject类型,其通过_resolvedAlignment字段持有Alignment的实例,Alignment就是Align对子节点位置的抽象表示。Algin实际持有的是AlignmentGeometry,它有多个子类,例如AlignmentDirectionalFractionalOffset,它们的主要差异在于坐标系的不同,具体可见图6-5和图6-6,在布局阶段,它们将统一转换为Alignment的布局进行处理。

这里以Alignment为例进行分析,其逻辑如代码清单6-4所示。

// 代码清单6-4 flutter/packages/flutter/lib/src/painting/alignment.dart

Alignment resolve(TextDirection? direction) => this;

alignChild方法最终会调用AlignmentalongOffset方法完成子节点偏移值的计算,如代码清单6-5所示。

// 代码清单6-5 flutter/packages/flutter/lib/src/painting/alignment.dart
Offset alongOffset(Offset other) {
   
   
  final double centerX = other.dx / 2.0; // 定位坐标系的原点
  final double centerY = other.dy / 2.0; // centerX、centerY为单位距离
  return Offset(centerX + x * centerX, centerY + y * centerY);  // 根据定位坐标系的坐标计算出在原始坐标系中对应的坐标,并作为偏移值返回
} 

以上逻辑中,参数other表示父节点大小减去子节点后剩余的偏移值,即图6-5中原始坐标系的A点,其在原始坐标系中的坐标为(other.dx, other.dy)。由return语句可知,最终的定位坐标系(图6-5中的虚线坐标系)会在原始坐标系的基础上在X、Y轴上各移动other的一半距离。此时,定位坐标系原点在原始坐标系中的坐标为(centerX,centerY),即O2点,原始坐标系的原点O1在定位坐标系中位置为(–1,–1)
图6-5 Align布局模型

由图6-5可知,O1(–1,–1)即父节点的左上角(topLeft),如代码清单6-6所示。Alignment的常量其实都是一些特殊坐标。

// 代码清单6-6 flutter/packages/flutter/lib/src/painting/alignment.dart
static const Alignment topLeft = Alignment(-1.0, -1.0);  // 见图6-5,O1点,左上角
static const Alignment topCenter = Alignment(0.0, -1.0);
static const Alignment topRight = Alignment(1.0, -1.0); // 见图6-5,B点,右上角
static const Alignment centerLeft = Alignment(-1.0, 0.0);
static const Alignment center = Alignment(0.0, 0.0); // 见图6-5,O2点,中点
static const Alignment centerRight = Alignment(1.0, 0.0);
static const Alignment bottomLeft = Alignment(-1.0, 1.0);
static const Alignment bottomCenter = Alignment(0.0, 1.0);
static const Alignment bottomRight = Alignment(1.0, 1.0); // 见图6-5,A点,右下角

以上是Alignment的坐标系中常用位置的坐标。FractionalOffsetAlignmentDirectional的功能类似,只是坐标系相对父节点的位置不同,如图6-6所示。

图6-6 AlignmentGeometry布局对比

事实上,通过坐标系就可以推断出AlignmentGeometry不同子类的实现细节,在此不再赘述。在实际开发中,应该根据业务场景选择合适的坐标系,而不是一味地借助Alignment进行Widget的定位。

Flex布局流程分析

本节分析Flex布局。Flex思想在前端领域由来已久,它为有限二维空间内的布局提供了一种灵活且高效的解决方案。Flex关键类的关系如图6-7所示,Flex是Flutter中行(Row)和列(Column)布局的基础和本质。

图6-7 Flex关键类

图6-7中,ColumnRow是常见的支持弹性布局的Widget,它们都继承自Flex,而Flex对应的RenderObjectRenderFlexRenderFlex实现弹性布局的关键,在于其子节点的parentData字段的类型为FlexParentData,其内部含有子节点的弹性系数(flex)等信息。需要注意的是,RenderFlex控制的是子节点的parentData字段的类型,而不是自身的字段,因而不是简单的重写(override)可以解决的,其类定义充分利用了Dart的mixin特性和泛型语法,远比图6-7所体现的关系要复杂。

由图6-7可知,行、列的布局的底层逻辑都将由RenderFlex统一完成,因此首先分析RenderFlexperformLayout方法,如代码清单6-7所示。

// 代码清单6-7 flutter/packages/flutter/lib/src/rendering/flex.dart
void performLayout() {
   
   
  final BoxConstraints constraints = this.constraints;
  final _LayoutSizes sizes = _computeSizes( // 第1步,对子节点进行布局,见代码清单6-8
    layoutChild: ChildLayoutHelper.layoutChild, // 子节点布局函数,即child.layout,
                                                // 见代码清单5-58
    constraints: constraints,); // 当前节点(RenderFlex)给子节点的约束条件
  final double allocatedSize = sizes.allocatedSize; // 所有子节点占用的空间大小
  double actualSize = sizes.mainSize;
  double crossSize = sizes.crossSize;
  // 第2步,交叉轴大小的校正,见代码清单6-12
  // 第3步,计算每个子节点在主轴的偏移值,见代码清单6-13
  // 第4步,计算每个子节点在交叉轴的偏移值,见代码清单6-14
}

以上逻辑可分为4步。第1步,执行每个子节点的Layout流程,计算出子节点所需要占用的空间大小,即主轴方向(即行的水平方向,列的垂直方向)的大小之和。此外,还将计算出交叉轴方向(即行垂直的方向,列的水平方向)的大小,取所有子节点中交叉轴方向最大值。第2步,对于交叉轴方向对齐方式为CrossAxisAlignment.baseline的情况,重新计算交叉轴方向的大小。这种情况不能简单取交叉轴方向上的最大值,这部分内容后面将详细分析。第3步,根据主轴的对齐方式,确定布局的起始位置和间距。第4步,依次完成每个子节点的布局,即计算每个子节点的偏移值。

首先分析第1步,其逻辑如代码清单6-8所示。

// 代码清单6-8 flutter/packages/flutter/lib/src/rendering/flex.dart
_LayoutSizes _computeSizes( ...... ) {
   
   
  int totalFlex = 0;
  final double maxMainSize = // 计算在当前约束下主轴方向的最大值
    _direction == Axis.horizontal ?  constraints.maxWidth  : constraints.maxHeight;
  final bool canFlex = maxMainSize < double.infinity; // 在约束为infinity的情况下,
                                                   // 弹性布局没有意义
  double crossSize = 0.0;
  double allocatedSize = 0.0; // 分配给非弹性节点(non-flexible)的总大小
  RenderBox? child = firstChild;
  RenderBox? lastFlexChild; // 最后一个Flex类型子节点,使用方式见代码清单6-10
  // 计算每个非Flex子节点占用空间的大小和弹性系数之和,见代码清单6-9
  // 根据剩余空间,计算每个Flex子节点占用空间的大小,见代码清单6-10
  final double idealSize = canFlex && mainAxisSize == MainAxisSize.max
// 最终的mainSize
     ? maxMainSize : allocatedSize; // 根据MainAxisSize类型计算主轴的实际大小
  return _LayoutSizes(mainSize: idealSize, crossSize: crossSize, allocatedSize: 
      allocatedSize, );
}

以上逻辑中,水平方向(Axis.horizontal)即Row的布局,垂直方向即Column的布局。canFlex表示是否可以执行弹性布局,仅当主轴大小为有限值时才可以,因为无限大(infinity)的值除以任意弹性系数,其值仍为无限大,因此此时没有意义。corssSize表示交叉轴的大小,即Row的高度和Column的宽度。

首先计算每个非Flex子节点占用空间的大小,如代码清单6-9所示。

// 代码清单6-9 flutter/packages/flutter/lib/src/rendering/flex.dart
while (child != null) {
   
   
  final FlexParentData childParentData = child.parentData! as FlexParentData;
  final int flex = _getFlex(child); 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

川峰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值