第4章 - 布局类组件
- 作者: 杜文 wendux
- 原文: https://book.flutterchina.club
- 本文仅用于本人学习和存档, 文章所有权归作者 杜文 wendux 所有, 如有侵权请发送邮件至 me#alanwei.com(#换成@) 删除此文.
4.1 布局类组件简介
Widget | 说明 | 用途 |
---|---|---|
LeafRenderObjectWidget | 非容器类组件基类 | Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image. |
SingleChildRenderObjectWidget | 单子组件基类 | 包含一个子Widget,如: ConstrainedBox、DecoratedBox等 |
MultiChildRenderObjectWidget | 多子组件基类 | 包含多个子Widget,一般都有一个children参数,接受一个Widget数组. 如Row、Column、Stack等 |
组件继承关系 Widget
> RenderObjectWidget
> (Leaf/SingleChild/MultiChild)RenderObjectWidget
RenderObjectWidget
类中定义了创建、更新RenderObject
的方法,子类必须实现他们,关于RenderObject
我们现在只需要知道它是最终布局、渲染UI界面的对象即可,也就是说,对于布局类组件来说,其布局算法都是通过对应的RenderObject
对象来实现的,所以读者如果对接下来介绍的某个布局类组件的原理感兴趣,可以查看其对应的RenderObject
的实现,比如Stack
(层叠布局)对应的RenderObject
对象就是RenderStack
,而层叠布局的实现就在RenderStack
中.
4.2 布局原理与约束(constraints)
尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBox
、SizedBox
、UnconstrainedBox
、AspectRatio
等.
Flutter 中有两种布局模型:
- 基于 RenderBox 的盒模型布局.
- 基于 Sliver ( RenderSliver ) 按需加载列表布局.
两种布局方式在细节上略有差异,但大体流程相同,布局流程如下:
- 上层组件向下层组件传递约束(constraints)条件.
- 下层组件确定自己的大小,然后告诉上层组件. 注意下层组件的大小必须符合父组件的约束.
- 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小).
比如,父组件传递给子组件的约束是“最大宽高不能超过100,最小宽高为0”,如果我们给子组件设置宽高都为200,则子组件最终的大小是100*100,因为任何时候子组件都必须先遵守父组件的约束,在此基础上再应用子组件约束(相当于父组件的约束和自身的大小求一个交集).
盒模型布局组件有两个特点:
- 组件对应的渲染对象都继承自 RenderBox 类. 在本书后面文章中如果提到某个组件是 RenderBox,则指它是基于盒模型布局的,而不是说组件是 RenderBox 类的实例.
- 在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述.
BoxConstraints
BoxConstraints
是盒模型布局过程中父渲染对象传递给子渲染对象的约束信息,子组件大小需要在约束的范围内, BoxConstraints
默认的构造函数如下:
const BoxConstraints({
this.minWidth = 0.0, // 最小宽度
this.maxWidth = double.infinity, //最大宽度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
BoxConstraints
还定义了一些便捷的构造函数:
BoxConstraints.tight(Size size)
它可以生成固定宽高的限制BoxConstraints.expand()
可以生成一个尽可能大的用以填充另一个容器的BoxConstraints
ConstrainedBox
ConstrainedBox
用于对子组件添加额外的约束, 比如:
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity, minHeight: 50),
child: Container(
height: 10,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red)
)
)
);
上面的代码会产生一个高度50、宽度100%的红色色块.
我们虽然将 Container
的高度设置为10像素,但是最终却是50像素,这正是ConstrainedBox
的最小高度限制生效了. 如果将Container的高度设置为80像素,那么最终红色区域的高度也会是80像素,因为ConstrainedBox
只限制了最小高度50像素,并未限制最大高度(最大高度默认是 double.infinity
).
SizedBox
SizedBox
用于给子元素指定固定的宽高, 实际上SizedBox
只是ConstrainedBox
的一个定制:
SizedBox(
width: 80.0,
height: 80.0,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red)
)
)
// 等价于
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red)
),
)
// 最终等价于
ConstrainedBox(
constraints: BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red)
),
)
多重限制
如果某一个组件有多个父级ConstrainedBox限制时,对于minWidth
和minHeight
来说,是取父子中相应数值最大的. 而对于maxWidth
和maxHeight
取多个ConstrainedBox约束中最小的. 只有这样才能保证父限制与子限制不冲突.
UnconstrainedBox
假如有一个组件 A,它的子组件是B,B 的子组件是 C,则 C 必须遵守 B 的约束,同时 B 必须遵守 A 的约束,也就是说正常情况下组件的约束会向下传递. 但是 A 的约束不会直接约束到 C,除非B将A对它自己的约束透传给了C, 利用这个原理, 可以实现一个这样的 B 组件:
- B 组件中在布局 C 时不约束C(可以为无限大).
- C 根据自身真实的空间占用来确定自身的大小.
- B 在遵守 A 的约束前提下结合子组件的大小确定自身大小.
而这个B组件就是 UnconstrainedBox
组件,也就是说 UnconstrainedBox
的子组件将不再受到约束,大小完全取决于自己. 比如:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
child: UnconstrainedBox( //去除父级限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), //子
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
),
),
),
)
如果没有中间的 UnconstrainedBox
,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框. 但是由于UnconstrainedBox
“去除”了父ConstrainedBox
的限制,则最终会按照子ConstrainedBox
的限制来绘制DecoratedBox
,即90×20.
但是,UnconstrainedBox
对父组件限制的“去除”并非是真正的去除: 上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间. 也就是说父限制的minHeight(100.0)
仍然是生效的,只不过它不影响最终子元素DecoratedBox
的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox
是作用于子UnconstrainedBox
上,而DecoratedBox
只受子ConstrainedBox
限制.
任何时候子组件都必须遵守其父组件的约束, 没有方法可以彻底去除父ConstrainedBox
的限制.
UnconstrainedBox 虽然在其子组件布局时可以取消约束(子组件可以为无限大),但是 UnconstrainedBox 自身是受其父组件约束的,所以当 UnconstrainedBox 随着其子组件变大后,如果UnconstrainedBox 的大小超过它父组件约束时,也会导致溢出报错,比如把上面那段代码里父BoxConstraints设置一个maxWidth
小于子ConstrainedBox的最小宽,就会出现导致溢出报错:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, maxWidth: 80, minHeight: 100.0), //父
child: UnconstrainedBox( //去除父级限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0), // 这里的最小宽超过了父级约束的最大宽
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
),
),
),
)
其它约束类容器
- AspectRatio: 它可以指定子组件的长宽比
- LimitedBox: 用于指定最大宽高
- FractionallySizedBox: 可以根据父容器宽高的百分比来设置子组件宽高
4.3 线性布局(Row和Column)
Flutter 中通过Row和Column来实现线性布局, 都继承自Flex.
主轴和纵轴
对于线性布局, 有主轴和纵轴之分:
- 如果布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向
- 如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向. 在线性布局中
有两个定义对齐方式的枚举类MainAxisAlignment
和CrossAxisAlignment
,分别代表主轴对齐和纵轴对齐.
Row
Row可以沿水平方向排列其子widget. 定义如下:
Row({
...
TextDirection textDirection,
MainAxisSize mainAxisSize = MainAxisSize.max,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
VerticalDirection verticalDirection = VerticalDirection.down,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
List<Widget> children = const <Widget>[],
})
textDirection
表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)
mainAxisSize
表示Row在主轴(水平)方向占用的空间
- 默认是 MainAxisSize.max, 表示尽可能多的占用水平方向的空间, 此时无论子 widgets 实际占用多少水平空间, Row的宽度始终等于水平方向的最大宽度;
- 而 MainAxisSize.min 表示尽可能少的占用水平空间, 当子组件没有占满水平剩余空间 , 则Row的实际宽度等于所有子组件占用的的水平空间;
mainAxisAlignment
表示子组件在Row
所占用的水平空间内对齐方式.
如果mainAxisSize
值为MainAxisSize.min
, 子组件的宽度等于Row
的宽度, 此属性无意义. 只有当mainAxisSize
的值为MainAxisSize.max
时, 此属性才有意义.
MainAxisAlignment.start
表示沿textDirection
的初始方向对齐, 如textDirection
取值为TextDirection.ltr
时, 则MainAxisAlignment.start
表示左对齐,textDirection
取值为TextDirection.rtl
时表示从右对齐.MainAxisAlignment.end
和MainAxisAlignment.start
正好相反MainAxisAlignment.center
表示居中对齐
读者可以这么理解: textDirection
是mainAxisAlignment
的参考系.
verticalDirection
表示Row纵轴(垂直)的对齐方向,默认是VerticalDirection.down
,表示从上到下。
crossAxisAlignment
表示子组件在纵轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.down时crossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.end和crossAxisAlignment.start正好相反;
Column
Column可以在垂直方向排列其子组件。参数和Row一样,不同的是布局方向为垂直,主轴纵轴正好相反.
Row(主轴mainAxis为水平方向)和Column(主轴mainAxis为垂直方向), 都只会在主轴方向占用尽可能大的空间, 而纵轴的长度则取决于他们最大子元素的长度.
比如:
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
)
效果如下:
虽然我们指定纵轴居中对齐:crossAxisAlignment: CrossAxisAlignment.center
(Column的纵轴为水平方向), 但是两行文本并未在屏幕水平方向的中间, 是因为Column和Row一样, 默认只会在主轴方向占尽可能大的空间, 而纵轴的长度则取决于他们最大子元素的长度, 对于上述代码, Colum的宽度实际上等同于第二行文字的宽度, 所以第一行文字位于第二行文本宽度的中间. 借助ConstrainedBox或SizedBox可以强制更改Column纵轴方向的宽度:
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
),
);
效果如下:
特殊情况
如果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(
mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
)
)
],
),
),
)
4.4 弹性布局(Flex)
Flex
Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column:
Flex({
...
required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
})
Expanded
Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为 Row和Column 都继承自 Flex,所以 Expanded 也可以作为它们的孩子:
const Expanded({
int flex = 1,
required Widget child,
})
flex
flex参数为弹性系数,如果为 0 或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其 flex 的比例来分割主轴的全部空闲空间。
Spacer
Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类,Spacer的源码如下:
class Spacer extends StatelessWidget {
const Spacer({Key? key, this.flex = 1})
: assert(flex != null),
assert(flex > 0),
super(key: key);
final int flex;
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
}
4.5 流式布局
Flutter中通过Wrap和Flow来支持流式布局
Wrap
Wrap的很多属性在Row(包括Flex和Column)中也有,这些参数意义是相同的:
Wrap({
...
this.direction = Axis.horizontal,
this.alignment = WrapAlignment.start,
// 主轴方向子widget的间距
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>[],
})
Flow
4.6 层叠布局
层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。
Stack
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.clipBehavior = Clip.hardEdge,
List<Widget> children = const <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的大小。
Stack 的子元素是堆叠的, children后面的元素会覆盖children前面的元素.
clipBehavior
此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,Clip.hardEdge
表示直接剪裁,不应用抗锯齿,更多信息可以查看源码注释。
Positioned
const Positioned({
Key? key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
required Widget child,
})
left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。width和height用于指定需要定位元素的宽度和高度。注意,Positioned的width、height 和其它地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。
示例:
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
// 表示子元素默认 顶部居中 显示(topCenter, 指的是竖轴方向顶部对齐: top, 横轴方向居中对齐: Center)
alignment: Alignment.topCenter,
children: [
/**
* 非 Positioned 组件, 定位遵循 Stack.alignment,
* 也就是 顶部居中 显示
*/
Container(
child: Text(
"Container",
style: TextStyle(color: Colors.red),
),
),
/**
* 部分定位的 Positioned,
* 由于未指定纵轴方向(top/bottom)的距离, 遵循 Alignment.topCenter 中的 top,
* 由于指定了横轴方向的left值, 所以覆盖了 Alignment.topCenter 中的 Center,
* 最终显示效果是距左 20 像素, 距离顶部 0 像素
*/
Positioned(
child: Text(
"Position with left 20",
style: TextStyle(color: Colors.blue),
),
left: 20,
),
/**
* 部分定位的 Positioned,
* 由于指定了纵轴方向的top值, 所以覆盖了 Alignment.topCenter 中的 top
* 由于未指定了横轴方向(left/right)的距离, 所以遵循 Alignment.topCenter 中的 Center
* 最终显示效果是距顶部 20 像素, 然后横轴方向居中
*/
Positioned(
child: Text(
"Position with top 20",
style: TextStyle(color: Colors.green),
),
top: 20,
),
// 完全定位的 Positioned, 忽略 Stack.alignment 指定的 Alignment.topCenter
Positioned(
child: Text("Position with top and left"),
top: 40,
left: 40,
)
],
),
)
效果如下:
4.7 对齐与相对定位
Align
Align
组件可以调整子组件的位置,定义如下:
Align({
Key key,
/**
* 需要一个AlignmentGeometry类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:Alignment和 FractionalOffset
*/
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget child,
})
alignment
需要一个AlignmentGeometry
类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry
是一个抽象类,它有两个常用的子类:Alignment
和FractionalOffset
widthFactor
和heightFactor
是用于确定Align
组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align
组件的宽高。如果值为null
,则组件的宽高将会占用尽可能多的空间。
比如:
Container(
height: 120.0,
width: 120.0,
color: Colors.bule.shade50,
child: Align(
alignment: Alignment.topRight,
child: FlutterLogo(
size: 60,
),
),
)
上面的代码中, 蓝色背景的容器Container
宽高各120像素, 由于Align
没有设置widthFactor
和heightFactor
, 所以Align
撑满Container
的空间, FlutterLogo
位于Align
的右上角, 同时也是Container
的右上角.
上面代码的效果也等同于如下:
Container(
color: Colors.bule.shade50,
child: Align(
widthFactor: 2,
heightFactor: 2,
alignment: Alignment.topRight,
child: FlutterLogo(
size: 60,
),
),
)
Align
的widthFactor
为2, 其子元素的宽为60, Align
元素的实际宽就等于 2 * 60 = 120px, 效果如下:
FractionalOffset
FractionalOffset
继承自 Alignment
,它和 Alignment
唯一的区别就是坐标原点不同. Aligment
的坐标系原点是在父控件的中间, 而 FractionalOffset
的坐标原点为矩形的左侧顶点:
FractionalOffset(dx, dy)
dx
和dy
是一个倍数而不是距离值dx
表示子控件从父容器的最左边移动到最右边的距离, 值为dx * (parentWidth - childWidth)
dy
表示子控件从父容器最顶部移动到最底部的距离, 值为dy * (parentHeight - childHeight)