Flutter进行布局时,上层 widget 会向下层 widget 传递一个尺寸约束,下层 widget 不能违反父级尺寸约束。当子级约束和父级约束有冲突时,子级约束无效,按照父级约束。
什么是布局约束?
Flutter进行布局时,上层 widget 会向下层 widget 传递一个尺寸约束,下层 widget 不能违反父级尺寸约束。当子级约束和父级约束有冲突时,子级约束无效,按照父级约束。(爸爸说的才算数。。。😂)
怎么获取布局约束?
对布局约束不明确可以用LayoutBuilder去获取上层传递的约束,也可以在看一下Flutter布局(二)常见布局组件的布局约束。
LayoutBuilder( builder: (context, constraints) { print('$constraints'); return Text( 'HomeView is working', ); } )
严格约束(Tight) vs 宽松约束(loose)
严格约束:宽高的最小值和最大值相等。
BoxConstraints.tight(Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height;
宽松约束: 宽高最小值都是0。
BoxConstraints.loose(Size size) : minWidth = 0.0, maxWidth = size.width, minHeight = 0.0, maxHeight = size.height;
在宽松约束下,子级约束可以在约束区间内自由定义;在严格约束下,子级必须遵守此约束。
布局约束流程
首先,上层 widget 向下层 widget 传递约束条件。 然后,下层 widget 向上层 widget 传递大小信息。 最后,上层 widget 决定下层 widget 的位置。 一句话就是向下传递约束,向上传递尺寸。
为了加深我们对Flutter布局约束的理解,下面用CustomMultiChildLayout来实现一个自定义布局的例子。
CustomMultiChildLayout( delegate: ColumnDelegate(), children: [ //Z-Order类似Stack,越往后的子级显示在最上面 LayoutId( id: 1, child: Container(width: 10, height: 10, color: Colors.amber), ), LayoutId( id: 2, child: Container(width: 10, height: 10, color: Colors.amber), ), LayoutId( id: 3, child: Container(width: 10, height: 10, color: Colors.amber), ), ], ) class ColumnDelegate extends MultiChildLayoutDelegate { //自定义布局实现 @override void performLayout(Size size) { //向下传递约束BoxConstraints.loose(size),返回子级的尺寸 var firstSize = layoutChild(1, BoxConstraints.loose(size)); //设置子级的位置 positionChild(1, const Offset(0, 0)); var secondSize = layoutChild(2, BoxConstraints.loose(size)); positionChild(2, const Offset(0, firstSize.height)); layoutChild(3, BoxConstraints.loose(size)); positionChild(3, const Offset(0, secondSize.height)); } //是否需要重新调用performLayout @override bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate) { return true; } //组件尺寸大小 @override Size getSize(BoxConstraints constraints) { return constraints.smallest; } }
下面举一个实际的布局约束流程:
Scaffold( body: Container( width: 300, height: 300, child: ConstrainedBox( constraints: BoxConstraints.loose(const Size.square(200)), child: SizeBox( width: 200, height: 200, child: Column( children: const [ SizedBox.expand(),//SizedBox1 SizedBox(width: 100,height: 100),//SizedBox2 SizedBox(width: 100,height: 100),//SizedBox3 ], ) ), ), ), )
-
Scaffold : 向下级传递一个宽松约束BoxConstraints(0.0<=w<=屏幕宽度, 0.0<=h<=屏幕高度)
-
Container :向下级传递一个严格约束BoxConstraints(w=300, h=300)
-
ConstrainedBox :上级传递的是一个严格约束,所以自己定义的布局约束无效,向下级传递一个严格约束BoxConstraints(w=300, h=300)
-
SizeBox : 上级传递的是一个严格约束,所以自己定义的布局约束无效,向下级传递一个严格约束BoxConstraints(w=300, h=300)
-
Column :SizedBox1是弹性控件,暂时无法指定约束,先跳过;向SizedBox2传递一个宽松约束BoxConstraints(0.0<=w<=300, 0.0<=h<=Infinity);向SizedBox3传递一个宽松约束BoxConstraints(0.0<=w<=300, 0.0<=h<=Infinity)。
-
SizedBox3 :向上传递其尺寸是BoxConstraints(w=100, h=100)。
-
SizedBox2 :向上传递其尺寸是BoxConstraints(w=100, h=100)。
-
Column : 所有的非弹性控件的尺寸知道了,向SizedBox1传递约束BoxConstraints(0.0<=w<=300, 0.0<=h<=100)。
-
SizedBox1 :向上传递其尺寸是BoxConstraints(w=300, h=100)。
-
Column : 向上传递其尺寸是BoxConstraints(w=300, h=300)。
-
SizeBox : 向上传递其尺寸是BoxConstraints(w=300, h=300)。
-
ConstrainedBox : 向上传递其尺寸是BoxConstraints(w=300, h=300)。
-
Container : 向上传递其尺寸是BoxConstraints(w=300, h=300)。
-
Scaffold :向上传递其尺寸是BoxConstraints(w=屏幕宽度, h=屏幕高度)。
官网上的举例分析都是错的,太难了。。。😂
对Flutter布局约束的理解
子级约束由父级约束指定这种责任链的设计方式,主要是为了让子级能够在父级范围内有效的显示,这个和Android中的布局也是比较相像的。
常见的容器类组件
直接或间接继承SingleChildRenderObjectWidget和MultiChildRenderObjectWidget的组件。
Widget | 说明 | 用途 |
---|---|---|
LeafRenderObjectWidget | 非容器类组件基类 | Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。 |
SingleChildRenderObjectWidget | 单子组件基类 | 包含一个子Widget,如:ConstrainedBox、DecoratedBox等 |
MultiChildRenderObjectWidget | 多子组件基类 | 包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等 |
下面介绍几种常见的容器类组件它们是如何传递约束的,只研究影响组件布局的属性,其他属性不做过多研究。
Scaffold
其宽高是由系统指定,严格约束其宽高为屏幕大小,向下传递宽松约束,向上传递屏幕尺寸。
Scaffold( //body约束为BoxConstraints(0.0<=w<=屏幕宽度, 0.0<=h<=屏幕高度) body: Container( color: blue, ), )
ConstrainedBox
向下传递严格布局,向上传递指定的尺寸。
Scaffold( body: ConstrainedBox( constraints: BoxConstraints.tight(const Size.square(100)), //child约束为BoxConstraints(w=100, h=100) child: Container( color: Colors.blue, ), ) )
向下传递宽松布局,向上传递子级尺寸。
Scaffold( body: ConstrainedBox( constraints: BoxConstraints.loose(const Size.square(100)), //child约束为BoxConstraints(0.0<=w<=100, 0.0<=h<=100) child: Container( color: Colors.blue, ), ) )
Sizebox
向下传递严格约束,向上传递指定的尺寸。
Scaffold( body: SizedBox( width: 100, height: 100, //child约束为BoxConstraints(w=100, h=100) child: Container( color: blue, ) ) )
实际上SizedBox
只是ConstrainedBox
的一个定制,两者都是通过RenderConstrainedBox
来渲染的,上面SizeBox的代码等价于:
ConstrainedBox( constraints: BoxConstraints.tightFor(width: 100,height: 100), child: container, )
UnconstrainedBox
向下传递无限约束BoxConstraints(unconstrained),向上传递子级的尺寸,如果没有子级相同的子级的尺寸是0。
Scaffold( body: Container( width: 100, height: 100, child: UnconstrainedBox( //child约束为BoxConstraints(unconstrained) child: Container( color: blue, width: 200,//宽度超过父控件,会显示越界异常 height: 100, ) ) ) )
Align
向下传递宽松约束,向上传递父控件的最大尺寸(Align默认是一种 expand 效果)。和UnconstrainedBox不同,其布局约束在其父级约束范围内,而UnconstrainedBox是没有任何约束,所以去掉严格约束限制建议使用Align。
Scaffold( body: Container( width: 100, height: 100, child: Align( //左上角 alignment: Alignment(-1,1) //child约束为BoxConstraints(0.0<=w<=100, 0.0<=h<=100) child: Container( color: blue, ) ) ) )
Padding
将其布局约束减去padding向下传递,向上传递子级加padding的尺寸。
Scaffold( body: SizedBox( width: 100, height: 100, child: Padding( padding: const EdgeInsets.all(10), //child约束为BoxConstraints(w=80, h=80) child: Container( color: Colors.blue, ), ) ) )
Container
指定宽高,向下传递严格约束,向上传递指定的尺寸。
Scaffold( body: Container( width: 100, height: 100, //child约束为BoxConstraints(w=100, h=100) child: Container( color: blue, ) ) )
没指定任何约束条件且没有子级,如果是无界约束向上传递尺寸是0 ,如果是有界约束向上传递约束的最大尺寸。(Container里面封装了LimitedBox,没有子级是默认子级是BoxConstraints.expand())
Scaffold( //向上传递屏幕大小的尺寸 body: Container( color: blue, ) )
没指定任何约束条件,有子级,向上传递子级的尺寸。(所以可以用Container查看组件的尺寸)
Scaffold( //向上传递200*200尺寸 body: Container( color: blue, child: Container( width: 200, height: 200, ), ) )
指定 alignment 属性同 Align 控件约束。 指定 padding 属性同 Padding 控件约束。 指定 constraints 属性同 ConstraintsBox 控件约束。
LimitedBox
当父级是无界约束时向下传递备用约束。
Scaffold( body: Column( children: [ //child纵轴是无界约束,LimitedBox尺寸是屏幕宽*0 LimitedBox( maxWidth: 0,//w是无界约束时备用最大宽是0 maxHeight: 0,//h是无界约束时备用最大高是0 child: ConstrainedBox(constraints: const BoxConstraints.expand()), ) ] ) )
Column
设置 crossAxisAlignment 为 CrossAxisAlignment.stretch 时,纵轴向下传递严格约束。 mainAxisSize为 MainAxisSize.min时,向上传递的尺寸是BoxConstraints(w=屏幕宽度, h=子级高度之和);mainAxisSize为 MainAxisSize.max时,向上传递的尺寸是BoxConstraints(w=屏幕宽度, h=父控件高最大值)。(注意:上面的Column是在宽松约束下,如果是在严格约束下mainAxisSize也不起作用,向上传递的尺寸也将是严格约束的尺寸)
Scaffold( body: Column( //纵轴设置拉伸 crossAxisAlignment: CrossAxisAlignment.stretch, children: [ //child约束为BoxConstraints(w=屏幕宽度, 0.0<=h<=Infinity) Container( color: Colors.blue, ), ], ) )
其他值向下传递宽松约束。 mainAxisSize为 MainAxisSize.min时,向上传递的尺寸是BoxConstraints(w=子级宽度最大值, h=子级高度之和);mainAxisSize为 MainAxisSize.max时,向上传递的尺寸是BoxConstraints(w=子级宽度最大值, h=父控件高最大值)。
Scaffold( body: Column( //纵轴设置居中 crossAxisAlignment: CrossAxisAlignment.center, children: [ //child约束为BoxConstraints(0.0<=w<=屏幕宽度, 0.0<=h<=Infinity) Container( color: Colors.blue, ), ], ) )
弹性子级 主轴方向,Column会先向非弹性子级传递一个无界约束,非弹性子级返回尺寸后,再向弹性子级(Flexiable)传递一个严格约束。
Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( flex: 1, //child约束为BoxConstraints(0.0<=w<=屏幕宽度, h=屏幕高度) child: Container( color: Colors.blue, ), ), ], ) )
为什么经常会出现超出边界异常? 因为Column无法事先知道有几个子级,也无法知道每个控件的尺寸,为了尽可能展示子级的显示效果,向固定子级传递的约束是 0.0<=h<=Infinit,子级在主轴方向可以随意定义尺寸大小,导致超出父控件Column的约束。
ListView放到Column为什么会报异常? ListView在主轴方向需要一个有界约束,但是直接把ListView放到Column中是一个无界约束,所以需要把ListView放到一个弹性(Flexiable)布局中。
Stack
先布局非位置的组件,当非位置之间布局完成确定Stack尺寸后,再去布局位置组件(Positioned)。
全是位置子级的情况 向下传递宽松约束,向上传递约束的最大尺寸。
Scaffold( //向上传递尺寸是屏幕的尺寸 body: Stack( alignment: Alignment.center, //children约束为BoxConstraints(0.0<=w<=屏幕宽度, 0.0<=h<=屏幕高度) children: [ Position( top: 0, left: 0, //child约束为BoxConstraints(unconstrained) child: Container( color: Colors.blue, width: 200, height: 200, ), ), Position( top: 0, left: 0, child: Container( color: Colors.red, width: 100, height: 100, ), ), ], ) )
不全是位置子级的情况 fit为StackFit.expand时,向下传递严格约束,向上传递非位置子级尺寸的最大值。
var children = [ Position( top: 50, left: 50, child: Container( color: Colors.blue, width: 100, height: 100, ), ), Container( color: Colors.red, width: 100, height: 100, ), Container( color: Colors.amber, width: 200, height: 200, ), ];
Scaffold( //向上传递尺寸是屏幕尺寸 body: Stack( fit: StackFit.expand, alignment: Alignment.center, //children约束为BoxConstraints(w=屏幕宽度, h=屏幕高度) children: children, ) )
fit为StackFit.loose时,向下传递宽松约束,向上传递非位置子级尺寸的最大值。
Scaffold( //向上传递尺寸是200*200 body: ConstrainedBox( constraints: BoxConstraints(minWidth: 100,minHeight: 100) child: Stack( fit: StackFit.loose, alignment: Alignment.center, //children约束为BoxConstraints(0.0<=w<=屏幕宽度, 0.0<=h<=屏幕高度) children: children, ) ) )
fit为StackFit.passthrough时,向下传递父级约束,向上传递非位置子级尺寸的最大值。
Scaffold( //向上传递尺寸是200*200 body: ConstrainedBox( constraints: BoxConstraints(minWidth: 100,minHeight: 100) child: Stack( fit: StackFit.loose, alignment: Alignment.center, //children约束为BoxConstraints(100<=w<=屏幕宽度, 100<=h<=屏幕高度) children: children, ) ) )
(注意:上述情况当Stack父级是固定尺寸时除外,此时Stack尺寸是固定的,不受子级尺寸的影响)