flutter —— 布局原理与约束
一、布局模型
主要有两种布局模型:
① 基于 RenderBox 的盒模型布局。
② 基于 Sliver ( RenderSliver ) 按需加载列表布局。(Sliver 布局请看)
两种布局方式在细节上略有差异,但大体流程相同,布局流程如下:
- 上层组件向下层组件传递约束(constraints)条件。
- 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
- 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
二、盒模型的约束
注:若本文中提及到的约束为“无限大”,则表示其最大约束由父级决定。
1 约束模型
const BoxConstraints({
this.minWidth = 0.0, //最小宽度
this.maxWidth = double.infinity, //最大宽度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
严格约束
SizedBox(
width: 80.0,
height: 80.0,
child: redBox
)
// 等价于
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: redBox,
)
2 多重约束
多重约束下的子组件,组件的最小值以父级中最大的为准,最大值以父级中最小的为准。
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 30,
maxHeight: 180,
),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 290, maxHeight: 30),
child: SizedBox(
width: 550,
height: 550,
child: redBox,
),
),
)
渲染的结果是 30,30 大小的正方形
3 UnconstrainedBox
用于取消父级约束,即子组件将不再受到约束,大小完全取决于自己。
基本原理:假如有一个组件 A,它的子组件是B,B 的子组件是 C,则 C 必须遵守 B 的约束,同时 B 必须遵守 A 的约束,但是 A 的约束不会直接约束到 C,除非B将A对它自己的约束透传给了C。 利用这个原理,就可以实现一个这样的 B 组件。该组件满足下列条件:
① B 组件中在布局 C 时不约束C(可以为无限大)。
② C 根据自身真实的空间占用来确定自身的大小。
③ B 在遵守 A 的约束前提下结合子组件的大小确定自身大小。
通常 UnconstrainedBox 用于取消父组件的最小约束限制。因为 UnconstrainedBox 会随着子组件变大而变大,若 UnconstrainedBox 的大小超过它的父级约束时,就会导致溢出报错。
示例:
代码:
Column(
mainAxisSize: MainAxisSize.min,
children: [
//正常模式
Container(
color: Colors.blue,
height: 100,
width: 100,
),
SizedBox(height: 10),
//子组件受上级约束影响
Container(
color: Colors.blue,
constraints: BoxConstraints(
minWidth: 100,
minHeight: 100,
),
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
),
SizedBox(height: 10),
//通过中间件 UnconstrainedBox 取消约束影响
Container(
color: Colors.blue,
constraints: BoxConstraints(
minWidth: 100,
minHeight: 100,
),
child: UnconstrainedBox(
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
),
),
SizedBox(height: 10),
//通过中间件 Align 取消约束影响
Container(
color: Colors.blue,
constraints: BoxConstraints(
minWidth: 100,
minHeight: 100,
),
child: Align(
heightFactor: 1,
widthFactor: 1,
child: Container(
width: 50,
height: 50,
color: Colors.red,
),
),
),
],
)
4 OverflowBox
用于对子组件强加额外的约束,且强加的那部分约束可不受父约束限制。相较于 UnconstrainedBox 有两个区别,一是 OverflowBox 的自身大小不随着子组件变化而变化,而是尽可能大,但不超过父约束。二是 OverflowBox 的子组件可超过父级约束,且不报溢出错误(不过溢出部分无法响应事件)。
示例:
代码:
Column(
mainAxisSize: MainAxisSize.min,
children: [
//正常模式
Container(
color: Colors.blue,
height: 100,
width: 100,
),
SizedBox(height: 10),
//子组件受父级约束影响
Container(
color: Colors.blue,
constraints: BoxConstraints(
maxWidth: 100,
maxHeight: 100,
),
child: Container(
width: 200,
height: 100,
color: Colors.red,
),
),
SizedBox(height: 10),
//子组件最大宽度不受父级约束影响
Container(
color: Colors.blue,
constraints: BoxConstraints(
maxWidth: 100,
maxHeight: 100,
),
child: OverflowBox(
maxWidth: double.infinity,
child: Container(
width: 200,
height: 100,
color: Colors.red,
),
),
),
],
)
三、布局原理
例1:自定义 center 组件
class CustomCenter extends SingleChildRenderObjectWidget {
const CustomCenter({Key? key, required Widget child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter({RenderBox? child}) : super(child);
@override
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;
}
// 注:这部分代码在继承的父类 RenderShiftedBox 中
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
// 绘制子节点
final BoxParentData childParentData = child!.parentData! as BoxParentData;
context.paintChild(child!, childParentData.offset + offset);
}
}
}
例2:Row、Column 线性布局
对于子级组件
以 Row 组件为例,则水平方向不限,垂直方向尽可能大(但不能超出父级约束,如果父级约束不限制,则由自身大小决定)
对于自身
1)纵轴方向
大小来自子级最大的那个,但不能超出父级约束
2)主轴方向
如果主轴方向父级约束是无限大,或 mainAxisSize = MainAxisSize.min(Row,Column 默认此设置为 max),则主轴方向大小为 allocatedSize,即子组件在主轴方向大小的。否则,主轴方向大小为当前父级的最大约束。
示例:
① Row 的默认布局
Container(
constraints: BoxConstraints(
maxWidth: 210,
maxHeight: 220,
),
color: Colors.green,
// 子级约束:宽度不限,高度最大220。自身大小:宽度210,高度102。
child: Row(
children: [
Container(
width: 301,
height: 102,
color: Colors.red,
),
Container(
width: 50,
height: 50,
color: Colors.blue,
),
],
),
)
② Row 设置 主轴 MainAxisSize.min
// 自身大小等于子级中最大的大小,即 50
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 50,
color: Colors.blue,
),
],
)
③ 两个嵌套的 Row
Container(
constraints: BoxConstraints(
maxWidth: 210,
maxHeight: 220,
),
color: Colors.green,
child: Row(
children: [
// 内层 Row 的主轴方向上的父级约束不限,所以自身的宽度也是 50
Row(
children: [
Container(
width: 50,
height: 50,
color: Colors.blue,
),
],
),
],
),
)
④ 两个嵌套的 Row 里面使用 Expanded
Container(
width: 300,
height: 300,
color: Colors.green,
child: Row(
children: [
Row(
children: [
// 由于内层 Row 的主轴方向上无限制,子组件的 flex 大于 0 会报错(Flexible 或 Expanded 组件的 flex 默认设置为 1
// 报错信息是:RenderFlex children have non-zero flex but incoming width constraints are unbounded.
Expanded(
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
],
)
],
),
)
例3:OverflowBox 布局原理
待更新
例4:Positioned 布局原理
待更新
例5:Container 布局与 Align
参考文档
https://book.flutterchina.club/chapter14/layout.html#_14-4-1-单子组件布局示例-customcenter