三、常用布局控件
线性布局(Row和Column)
所谓线性布局,即指沿水平或垂直方向排列子组件。Row和Column就像css中flex布局的flex-direction
属性:row
(横向) 和 column
(纵向)排列子元素。
Row
属性 | 值 | 描述 |
---|---|---|
mainAxisAlignment | MainAxisAlignment.start | 控制子widget在主轴方向上的对齐方式(对于Row 是水平方向,对于Column 是垂直方向) |
crossAxisAlignment | CrossAxisAlignment.center | 控制子widget在交叉轴上的对齐方式(对于Row 是垂直方向,对于Column 是水平方向) |
children | [Text('123')] | 子元素 |
mainAxisSize | MainAxisSize.max | 用于确定Row 或Column 在主轴方向上应占用的空间大小默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的水平空间; |
verticalDirection | VerticalDirection.up | 它决定了子widget在垂直方向上的起始排列点,是从上往下还是从下往上排列,默认是VerticalDirection.down ,表示从上到下。 |
verticalDirection
- 对于
Column
:VerticalDirection.down
是默认值,表示子widget从上往下排列,第一个子widget位于顶部,后续的子widget依次向下排列;VerticalDirection.up
表示子widget从下往上排列,第一个子widget位于底部,后续的子widget依次向上排列。 Row
或其他水平布局widget:Row
纵轴(垂直)的对齐方向,默认是VerticalDirection.down
,表示从上到下。虽然verticalDirection
属性影响的是垂直方向,但它决定了在主轴(水平方向)对齐方式为start
或end
时,如何处理交叉轴(垂直方向)上的对齐:
Column(
crossAxisAlignment: CrossAxisAlignment.start, //测试Row对齐方式,排除Column默认居中对齐的干扰
children: <Widget>[
Row( // 1
mainAxisAlignment: MainAxisAlignment.center, // 主轴居中
children: <Widget>[
Text(" 主轴居中1 "),
Text(" 主轴居中2 "),
],
),
Row( // 2
mainAxisSize: MainAxisSize.min, // 主轴尽可能小,导致两个文本都紧靠左边
mainAxisAlignment: MainAxisAlignment.center, // 主轴居中
children: <Widget>[
Text("主轴尽可能小&主轴居中1"),
Text("主轴尽可能小&主轴居中2"),
],
),
Row( // 3
mainAxisAlignment: MainAxisAlignment.end, // TextDirection.rtl从右向左的顺序排列,导致MainAxisAlignment.end表示左对齐
textDirection: TextDirection.rtl, // 文本方向右对齐
children: <Widget>[
Text("主轴居右文本方向1"),
Text("主轴居右文本方向2"),
],
),
Row( // 4
crossAxisAlignment: CrossAxisAlignment.start, // 交叉轴居上
verticalDirection: VerticalDirection.up, // 竖直方向导致文本靠下
children: <Widget>[
Text(" 交居上竖直1", style: TextStyle(fontSize: 30.0),),
Text(" 交居上竖直2"),
],
),
],
)
- 第一个
Row
很简单,默认为居中对齐; - 第二个
Row
,由于mainAxisSize
值为MainAxisSize.min
,Row
的宽度等于两个Text
的宽度和,所以对齐是无意义的,所以会从左往右显示; - 第三个
Row
设置textDirection
值为TextDirection.rtl
,所以子组件会从右向左的顺序排列,而此时MainAxisAlignment.end
表示左对齐,所以最终显示结果就是图中第三行的样子; - 第四个 Row 测试的是纵轴的对齐方式,由于两个子 Text 字体不一样,所以其高度也不同,我们指定了
verticalDirection
值为VerticalDirection.up
,即从低向顶排列,而此时crossAxisAlignment
值为CrossAxisAlignment.start
表示底对齐。
Column
Column
可以在垂直方向排列其子组件。参数和Row
一样,不同的是布局方向为垂直,主轴纵轴正好相反
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
Container(
color: Colors.green,
child: Text("hello world", style: TextStyle(fontSize: 30.0),),
),
],
)
- 由于我们没有指定
Column
的mainAxisSize
,所以使用默认值MainAxisSize.max
,则Column
会在垂直方向占用尽可能多的空间,此例中会占满整个屏幕高度。 - 由于我们指定了
crossAxisAlignment
属性为CrossAxisAlignment.center
,那么子项在Column
纵轴方向(此时为水平方向)会居中对齐。注意,在水平方向对齐是有边界的,总宽度为Column
占用空间的实际宽度,而实际的宽度取决于子项中宽度最大的Widget。在本例中,Column
有两个子Widget,而显示“hello world”的Container
宽度最大,所以Column
的实际宽度则为绿色的宽度,所以居中对齐后前面的两个元素会居中对齐
与css的Flex不同, Row
和Column
都只会在主轴方向占用尽可能大的空间,而纵轴的长度则取决于他们最大子元素的长度。如果我们想让本例中的文本控件在整个手机屏幕中间对齐:
- 将
Column
的宽度指定为屏幕宽度;这很简单,我们可以通过ConstrainedBox
或SizedBox
(我们将在后面章节中专门介绍这两个Widget)来强制更改宽度限制,将minWidth
设为double.infinity
,可以使宽度占用尽可能多的空间:
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
),
);
- 用
Center
组件 - 修改
crossAxisAlignment
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, // 使子widget填满水平空间
children: <Widget>[
Text("hi", textAlign: TextAlign.center,), // 需要调整文本
Text("world", textAlign: TextAlign.center,),
Container(
color: Colors.green,
child: Text("hello world", style: TextStyle(fontSize: 30.0),),
),
],
),
特殊情况
如果Row
里面嵌套Row
,或者Column
里面再嵌套Column
,那么只有最外面的Row
或Column
会占用尽可能大的空间,里面Row
或Column
所占用的空间为实际大小,下面以Column
为例说明:
Container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
children: <Widget>[
Container(
color: Colors.red,
child: Column(
mainAxisSize: MainAxisSize.max,//无效,内层Colum高度为实际高度
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
)
],
),
),
);
如果要让里面的Column
占满外部Column
,可以使用Expanded
组件:
Container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
children: <Widget>[
Expanded(
child: Container(
color: Colors.red,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
),
)
],
),
),
),
弹性布局(Flex)
弹性布局允许子组件按照一定比例来分配父容器空间。弹性布局的概念在其他UI系统中也都存在,如 H5 中的弹性盒子布局。Flutter 中的弹性布局主要通过Flex和Expanded来配合实现。
Flex
Flex
是一个抽象类,是Row
和Column
的基类,用于定义一个灵活的布局模型。Flex
布局允许其子widget沿着主轴(main axis)线性排列,子widget的大小可以按比例分配。
Flex
的主要属性包括:
direction
:确定主轴的方向,水平或垂直mainAxisAlignment
和crossAxisAlignment
:控制子widget在主轴和交叉轴上的对齐方式children
:包含一系列子widget在实际使用中,通常不会直接使用
Flex
,而是使用其具体实现Row
(水平布局)或Column
(垂直布局)
Expanded
Expanded
是一个widget,它可以包裹一个子widget,并按照其在Flex
(如Row
或Column
)中的flex
因子来分配空间。使用Expanded
可以让子widget填充未被其他子widget占用的空间,或按比例分配空间。
Expanded
的主要属性包括:
flex
:一个整数,表示子widget的弹性系数,用于确定它占据的空间比例。默认值为1child
: widget
例子
基于Column
和Row
:
body: Column(
children: <Widget>[
Expanded(
flex: 1, // 占用剩余空间的1/3
child: Container(color: Colors.red),
),
Expanded(
flex: 2, // 占用剩余空间的2/3
child: Container(color: Colors.green),
),
Row(
children: <Widget>[
Expanded(
child: Container(color: Colors.blue, height: 100),
),
Container(color: Colors.yellow, width: 50, height: 100),
Expanded(
child: Container(color: Colors.blue, height: 100),
),
],
),
],
),
基于Flex
:
示例中的
Spacer
的功能是占用指定比例的空间,实际上它只是Expanded
的一个包装类body: Column(
children: <Widget>[
//Flex的两个子widget按1:2来占据水平空间
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.red,
),
),
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.green,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: SizedBox(
height: 100.0,
//Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.red,
),
),
Spacer(
flex: 1,
),
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.green,
),
),
],
),
),
),
],
)
流式布局(Wrap、Flow)
Wrap
和Flow
是Flutter中用于布局的两个高级widget,它们提供比传统的Row
和Column
更多的灵活性和复杂性。
Wrap
Wrap
布局可以在水平或垂直方向上排列其子widget,并且可以自动换行或换列。这使得Wrap
非常适合创建一个需要自适应空间的布局,比如标签组或者有多个子项且空间受限的情况。
可以认为
Wrap
和Flex
(包括Row
和Column
)除了超出显示范围后Wrap
会折行外,其他行为基本相同Wrap({
...
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
this.spacing = 0.0,
this.runAlignment = WrapAlignment.start,
this.runSpacing = 0.0,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
List<Widget> children = const <Widget>[],
})Wrap的很多属性在
Row
(包括Flex
和Column
)中也有,如direction
、crossAxisAlignment
、textDirection
、verticalDirection
等,这些参数意义是相同的。
主要属性:
direction
:主轴方向,可以是水平或垂直alignment
和runAlignment
:分别控制主轴和交叉轴上的对齐方式spacing
:子widget在主轴方向上的间距runSpacing
:子widget在纵轴(交叉轴)方向上的间距,即行与行之间的间距
示例:
body: Wrap(
spacing: 8.0, // 主轴(水平)方向间距
runSpacing: 4.0, // 纵轴(垂直)方向间距
alignment: WrapAlignment.center, // 沿主轴方向居中
children: <Widget>[
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
label: Text('Hamilton'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: Text('Lafayette'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: Text('Lafayette'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: Text('Lafayette'),
),
// 更多Chip...
],
)
Flow
Flow
是一个提供高性能自定义布局策略的widget。与Wrap
相比,Flow
更加复杂和灵活,但也更难以使用。Flow
需要开发者重写FlowDelegate
来自定义布局策略。
我们一般很少会使用
Flow
,因为其过于复杂,需要自己实现子 widget 的位置转换,在很多场景下首先要考虑的是Wrap
是否满足需求。Flow
主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。
看到这里,就没空实践了~ 4.5 流式布局(Wrap、Flow) | 《Flutter实战·第二版》
Flow
有如下优点:
- 性能好;
Flow
是一个对子组件尺寸以及位置调整非常高效的控件,Flow
用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow
定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate
中的paintChildren()
方法中调用context.paintChild
进行重绘,而context.paintChild
在重绘时使用了转换矩阵,并没有实际调整组件位置。 - 灵活;由于我们需要自己实现
FlowDelegate
的paintChildren()
方法,所以我们需要自己计算每一个组件的位置,因此,可以自定义布局策略。 缺点: - 使用复杂。
- Flow 不能自适应子组件大小,必须通过指定父容器大小或实现
TestFlowDelegate
的getSize
返回固定大小。
层叠布局(Stack、Positioned)
层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。
Flutter中使用Stack
和Positioned
这两个组件来配合实现绝对定位。Stack
允许子组件堆叠,而Positioned
用于根据Stack
的四个角来确定子组件的位置。
先看一个小例子:
Stack
包含了三个子widget:一个大的红色Container
作为背景,一个较小的绿色Container
通过Positioned
定位在红色Container
的左上角,最上层是一个Text
widget,同样通过Positioned
定位。
body: Stack(
alignment: Alignment.center, // 未定位widget对齐方式
children: <Widget>[
Container(
width: 300.0,
height: 300.0,
color: Colors.red,
),
Positioned(
left: 50.0,
top: 50.0,
child: Container(
width: 200.0,
height: 200.0,
color: Colors.green,
),
),
Positioned(
left: 100.0,
top: 100.0,
child: Text(
'Stack布局',
style: TextStyle(fontSize: 20.0, color: Colors.white),
),
),
],
),
Stack
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.clipBehavior = Clip.hardEdge,
List<Widget> children = const <Widget>[],
})
Stack
允许其子widget堆叠在一起。默认情况下,Stack
中的第一个子widget会位于底部,后面的子widget依次堆叠在上面。可以使用Stack
的alignment
属性来指定如何对齐这些子widget。
属性 | 描述 |
---|---|
alignment | 决定如何去对齐没有定位(没有使用Positioned )或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位:left 、right 为横轴,top 、bottom 为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位 |
textDirection | 和Row 、Wrap 的textDirection 功能一样,都用于确定alignment 对齐的参考系,即:textDirection 的值为TextDirection.ltr ,则alignment 的start 代表左,end 代表右,即从左往右 的顺序;textDirection 的值为TextDirection.rtl ,则alignment的start 代表右,end 代表左,即从右往左 的顺序 |
fit | 用于确定没有定位的子组件如何去适应Stack 的大小。StackFit.loose 表示使用子组件的大小,StackFit.expand 表示扩伸到Stack 的大小 |
clipBehavior | 决定对超出Stack 显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,Clip.hardEdge 表示直接剪裁,不应用抗锯齿,更多信息可以查看源码注释。Clip.none 表示允许堆叠的子组件超出父组件的范围 |
Stack
本身并不直接接受宽度和高度参数来设置其大小,将Stack
包裹在一个Container
或SizedBox
中,并在这个外部widget上设置宽度和高度。这是设置Stack
尺寸最直接的方法。
Positioned
Positioned
只能嵌套在Stack
中,如果在不想调整结构的情况下可以使用Align
布局
const Positioned({
Key? key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
required Widget child,
})
Positioned
用于给Stack
内的子widget指定具体的位置。通过Positioned
,可以控制子widget的左、顶、右、底边距,实现精确布局。
主要属性:
left
、top
、right
、bottom
:用于确定widget距离Stack
的四边的距离。width
、height
:可以指定Positioned
widget的宽度和高度。
示例: Stack
指定一个fit
属性
body: Stack(
alignment:Alignment.center ,
fit: StackFit.expand, //未定位widget占满Stack整个空间
children: <Widget>[
Positioned(
left: 18.0,
child: Text("被遮住了"),
),
Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
由于第二个子文本组件没有定位,所以fit
属性会对它起作用,就会占满Stack
。由于Stack
子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示
对齐与相对定位(Align、Center)
Align
widget 在 Flutter 中用于对齐和定位其子 widget。通过 Align
,你可以控制子 widget 在父 widget 中的具体位置,无论父 widget 的大小如何变化,Align
都能确保子 widget 根据设定的对齐方式定位。
Align({
Key key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget child,
})
Align
alignment | Align widget 的主要属性,用于确定子 widget 的对齐方式。alignment 属性接受一个 Alignment 对象,如 Alignment.center 、Alignment.topRight 、Alignment.bottomLeft 等。Alignment 类中预定义了一系列常用的对齐方式,同时你也可以通过 Alignment(x, y) 来自定义对齐方式,其中 x 和 y 是从 -1.0 到 1.0 的值,分别代表水平和垂直方向 |
widthFactor、heightFactor | 是用于确定Align 组件本身宽高的属性它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null ,则组件的宽高将会占用尽可能多的空间。 |
如果我们不显式指定宽高,而通过同时指定widthFactor
和heightFactor
为 2 也是可以达到同样的效果:
Align(
widthFactor: 2, // 因为`FlutterLogo`的宽高为 60,则`Align`的最终宽高都为`2*60=120`
heightFactor: 2,
alignment: Alignment.topRight,
child: FlutterLogo(
size: 60,
),
),
Align和Stack对比
可以看到,Align
和Stack
/Positioned
都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:
- 定位参考系统不同;
Stack
/Positioned
定位的参考系可以是父容器矩形的四个顶点;而Align
则需要先通过alignment
参数来确定坐标原点,不同的alignment
会对应不同原点,最终的偏移是需要通过alignment
的转换公式来计算出。 Stack
可以有多个子元素,并且子元素可以堆叠,而Align
只能有一个子元素,不存在堆叠。
Alignment
与css中的clip-path: polygon(0 0, 0 100%)
不同,css中的基点为左上角,而Alignment
为元素正中心。所以优先用FractionalOffset进行偏移布局更容易理解。
Alignment
Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)
。x
、y
的值从-1到1分别代表矩形左边到右边的距离和顶部到底边的距离,因此2个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0)
代表矩形的左侧顶点,而Alignment(1.0, 1.0)
代表右侧底部终点,而Alignment(1.0, -1.0)
则正是右侧顶点,即Alignment.topRight
。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment
类中都已经定义为了静态常量。
Alignment
可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:
实际偏移 = (Alignment.x * (parentWidth - childWidth) / 2 + (parentWidth - childWidth) / 2,
Alignment.y * (parentHeight - childHeight) / 2 + (parentHeight - childHeight) / 2)
示例: 最终计算的偏移结果为:(0, 30)
body: Container(
color: Colors.red,
child: Align(
widthFactor: 2,
heightFactor: 2,
alignment: Alignment(-1,0.0),
child: FlutterLogo(
size: 60,
),
),)
FractionalOffset
FractionalOffset
继承自 Alignment
,它和 Alignment
唯一的区别就是坐标原点不同!FractionalOffset
的坐标原点为矩形的左侧顶点,这和Css布局系统一致
FractionalOffset
的坐标转换公式为:
实际偏移 = (FractionalOffse.x * (parentWidth - childWidth), FractionalOffse.y * (parentHeight - childHeight))
Center
将其单个子 widget 居中对齐在其父 widget 中。Center
widget 可以在垂直方向、水平方向或者两者上同时居中子 widget。它非常适合用于需要简单居中布局的场景,如将文本、按钮、图标或自定义子 widget 居中显示。
Center
继承自Align
,它比Align
只少了一个alignment
参数;由于Align
的构造函数中alignment
值为Alignment.center
。 所以,我们可以认为Center
组件其实是对齐方式确定(Alignment.center
)了的Align
主要属性:
child
:Center
widget 的唯一子 widget。这个子 widget 将被居中对齐。widthFactor
和heightFactor
:这些是可选属性,用于确定Center
widget 自身的大小相对于其子 widget 的大小。如果设置了widthFactor
或heightFactor
,则Center
widget 的大小将是其子 widget 大小的倍数;如果未设置,Center
widget 将尽可能大地扩展以填满父 widget 的可用空间
Center( child: Text('这是一个居中的文本'), ),
LayoutBuilder、AfterLayout
LayoutBuilder
LayoutBuilder
是一个widget,它可以构建一个依赖于其父widget的尺寸的widget树。LayoutBuilder
会将父widget的约束传递给它的builder函数,你可以根据这些约束来决定如何构建子widget。这对于创建响应父widget大小变化的布局非常有用。
LayoutBuilder
的builder函数提供了一个BuildContext
和一个BoxConstraints
对象,后者描述了父widget对子widget的约束(例如,最大/最小宽度和高度)
示例
实现一个响应式的 Column 组件 ResponsiveColumn,它的功能是当当前可用的宽度小于 200 时,将子组件显示为一列,否则显示为两列:
class ResponsiveColumn extends StatelessWidget {
const ResponsiveColumn({Key? key, required this.children}) : super(key: key);
final List<Widget> children;
Widget build(BuildContext context) {
// 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth < 200) {
// 最大宽度小于200,显示单列
return Column(children: children, mainAxisSize: MainAxisSize.min);
} else {
// 大于200,显示双列
var _children = <Widget>[];
for (var i = 0; i < children.length; i += 2) {
if (i + 1 < children.length) {
_children.add(Row(
children: [children[i], children[i + 1]],
mainAxisSize: MainAxisSize.min,
));
} else {
_children.add(children[i]);
}
}
return Column(children: _children, mainAxisSize: MainAxisSize.min);
}
},
);
}
}
class LayoutBuilderRoute extends StatelessWidget {
const LayoutBuilderRoute({Key? key}) : super(key: key);
Widget build(BuildContext context) {
var _children = List.filled(6, Text("A"));
// Column在本示例中在水平方向的最大宽度为屏幕的宽度
return Column(
children: [
// 限制宽度为190,小于 200
Container(
width: 190,
color: Colors.green,
child: ResponsiveColumn(children: _children),
),
ResponsiveColumn(children: _children),
// LayoutLogPrint(child:Text("xx")) // 下面介绍
],
);
}
}
它非常实用且重要,它主要有两个使用场景:
- 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。
- LayoutBuilder 可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时 LayoutBuilder 就很有用。
打印布局时的约束信息
注意!我们的大前提是盒模型布局,如果是Sliver 布局,可以使用 SliverLayoutBuiler 来打印。
class LayoutLogPrint<T> extends StatelessWidget {
const LayoutLogPrint({
Key? key,
this.tag,
required this.child,
}) : super(key: key);
final Widget child; final T? tag; //指定日志tag
@override Widget build(BuildContext context) { return LayoutBuilder(builder: (_, constraints) { // assert在编译release版本时会被去除 assert(() { print('${tag ?? key ?? child}: $constraints'); return true; }()); return child; }); } }
LayoutLogPrint(child:Text("xx"))
![](../../static/docs/Pasted%20image%2020240131180041.jpeg)
可以看到 Text("xx") 的显示空间最大宽度为 393,最大高度未做限制 。
### AfterLayout
`AfterLayout`是一个Dart mixin,不是一个widget。它提供了一种在widget布局完成后执行代码的方式。
> Flutter响应式UI框架,而命令式UI框架最大的不同就是:大多数情况下开发者只需要关注数据的变化,因此如果我们想在 Flutter 中获取某个组件的大小和位置就会很困难。个人理解`就像jQuery和Vue`的区别。
使用`AfterLayout` mixin时,你的widget状态类还需要扩展`State<T>`,其中`T`是你的widget类。`afterFirstLayout`方法在widget第一次布局完成后被调用,这对于执行依赖于widget大小的初始化任务非常有用。
简单示例:
```dart
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with AfterLayoutMixin<MyWidget> {
@override
void afterFirstLayout(BuildContext context) {
// 布局完成后的操作
}
@override
Widget build(BuildContext context) {
return Container(); // 你的widget布局
}
}