- 原文博客地址:
- 相关博客系列文章:
- 相关
Demo
地址: - 布局类
Widget
都会包含一个或多个子widget
,不同的布局类Widget
对子widget
排版(layout
)方式不同 - 中提到:
Widget
实际上就是Element
的配置数据,Widget
的功能是描述一个UI
元素的一个配置数据, 而真正的UI
渲染是由Element
构成 - 在
Flutter
中,根据Widget
是否需要包含子节点将Widget
分为了三类,分别对应三种Element
,如下表
Widget | 对应的Element | 用途 |
---|---|---|
LeafRenderObjectWidget | LeafRenderObjectElement | Widget 树的叶子节点,用于没有子节点的widget ,通常基础widget 都属于这一类,如Text 、Image |
SingleChildRenderObjectWidget | SingleChildRenderObjectElement | 包含一个子Widget ,如:ConstrainedBox 、DecoratedBox 等 |
MultiChildRenderObjectWidget | MultiChildRenderObjectElement | 包含多个子Widget ,一般都有一个children 参数,接受一个Widget 数组。如Row 、Column 、Stack 等 |
布局类Widget
- 布局类
Widget
就是指直接或间接继承(包含)MultiChildRenderObjectWidget
的Widget
,它们一般都会有一个children
属性用于接收子Widget
Widget
的继承关系如下:Widget
>RenderObjectWidget
>(Leaf/SingleChild/MultiChild)RenderObjectWidget
RenderObjectWidget
类中定义了创建、更新RenderObject
的方法,子类必须实现他们- 对于布局类
Widget
来说,其布局算法都是通过对应的RenderObject
对象来实现的 Flutter
中主要有以下几种布局类的Widget
:- 线性布局
Row
和Column
- 弹性布局
Flex
- 流式布局
Wrap
、Flow
- 层叠布局
Stack
、Positioned
- 线性布局
线性布局
Row
和Column
是一种现行布局的Widget
, 都继承自Flex
- 所谓线性布局,即指沿水平或垂直方向排布
子Widget
- 对于线性布局,有主轴和纵轴之分,如果布局是沿水平方,那么主轴就指是水平方向,而纵轴即垂直方向;如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向
Row
的主轴即为水平方向,Column
的主轴是垂直方向, 切两者的属性和使用都一样- 相关下定义的源码如下:
Row({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, Listchildren = const [],})Column({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, List children = const [],}) 复制代码
相关属性如下
mainAxisAlignment
子Widget
在主轴方向的排列方式, 为方便以下皆称Widget
为组件
// 默认值MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start复制代码
start
: 子widgets
向主轴起点对其, 依次排列end
: 子widgets
向主轴终点对其, 依次排列center
: 所有子widgets
居中排列spaceBetween
: 均匀分配,相邻widgets
间距离相同。每行第一个widgets
与行首对齐,每行最后一个widgets
与行尾对齐spaceAround
: 均匀分配,相邻widgets
间距离相同。每行第一个widgets
到行首的距离和每行最后一个widgets
到行尾的距离将会是相邻widgets
之间距离的一半spaceEvenly
: 均匀分配,相邻widgets
间距离相同。每行第一个widgets
到行首的距离和每行最后一个widgets
到行尾的距离和相邻widgets
之间距离相同
属性 | 效果 |
---|---|
start | |
end | |
center | |
spaceBetween | |
spaceAround | |
spaceEvenly |
mainAxisSize
// 默认值MainAxisSize mainAxisSize = MainAxisSize.max复制代码
- 表示
Row
在主轴(水平)方向占用的空间,默认是MainAxisSize.max
max
表示尽可能多的占用水平方向的空间,此时无论子widgets
实际占用多少水平空间,Row
的宽度始终等于水平方向的最大宽度;MainAxisSize.min
表示尽可能少的占用水平空间,当子widgets
没有占满水平剩余空间,则Row
的实际宽度等于所有子widgets
占用的的水平空间
verticalDirection
表示Row纵轴(垂直)的对齐方向, 默认值down
,表示从上到下; up
表示从下到上
// 默认值VerticalDirection verticalDirection = VerticalDirection.down复制代码
crossAxisAlignment
- 表示子
Widgets
在纵轴方向的对齐方式,Row
的高度等于子Widgets
中最高的子元素高度 crossAxisAlignment
的参考系是verticalDirection
// 默认值CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center/** * VerticalDirection.down时, crossAxisAlignment.start指顶部对齐 * VerticalDirection.up时,crossAxisAlignment.start指底部对齐 * crossAxisAlignment.end和crossAxisAlignment.start正好相反 */复制代码
- 当
VerticalDirection.down
时,crossAxisAlignment
个枚举值如下 start
: 顶部对其end
: 底部对其center
: 居中对其stretch
: 侧轴方向上, 子Widget
的高度拉伸至和Row
的高度相同baseline
: 不论VerticalDirection
取值如何, 子Widget
的顶部和Row
的顶部对其
textDirection
表示水平方向子widget的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)
TextDirection textDirection/** * ltr: 从左往右 * rtl: 从右往左 */复制代码
textBaseline
用于对其文本的水平线,
TextBaseline textBaseline/** * alphabetic: 用于对齐普通的字母基线 * ideographic: 用于对齐表意基线 */复制代码
使用代码
Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, textDirection: TextDirection.ltr, verticalDirection: VerticalDirection.down, textBaseline: TextBaseline.ideographic, children:[ new Container(width: 80.0, height:80.0, color: Colors.red,), new Container(width: 80.0, height:90.0, color: Colors.green,), new Container(width: 80.0, height:100.0, color: Colors.blue,), ], )复制代码
特别注意
在Row
和Column
中, 如果子widget
超出屏幕范围,则会报溢出错误
弹性布局
- 弹性布局允许子
widget
按照一定比例来分配父容器空间 Flutter
中的弹性布局主要通过Flex
和Expanded
来配合实现Flex
可以沿着水平或垂直方向排列子widget
- 如果已知主轴方向,建议使用
Row
或Column
,因为Row
和Column
都继承自Flex
,参数基本相同,所以能使用Flex
的地方一定可以使用Row
或Column
Flex
本身功能是很强大的,它也可以和Expanded
配合实现弹性布局,接下来我们只讨论Flex
和弹性布局相关的属性(其它属性已经在介绍Row
和Column
时介绍过了)
Flex
Flex({ Key key, //弹性布局的方向 @required this.direction, this.mainAxisAlignment = MainAxisAlignment.start, this.mainAxisSize = MainAxisSize.max, this.crossAxisAlignment = CrossAxisAlignment.center, this.textDirection, this.verticalDirection = VerticalDirection.down, this.textBaseline, Listchildren = const [],})// direction// 水平方向Axis direction = Axis.horizontal// 垂直方向, 默认为垂直方向Axis direction = Axis.vertical复制代码
Flex
继承自MultiChildRenderObjectWidget
,对应的RenderObject
为RenderFlex
,RenderFlex
中实现了其布局算法
Expanded
可以按比例缩放Row
、Column
和Flex
子widget
所占用的空间
class Expanded extends Flexible { const Expanded({ Key key, int flex = 1, @required Widget child, }) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);}复制代码
flex
为弹性系数,如果为0或null
,则child
是没有弹性的,即不会被扩伸占用的空间 如果大于0,所有的Expanded
按照其flex
的比例来分割主轴的全部空闲空间
Row( children:[ Container(width: 80.0, height:80.0, color: Colors.red,), Expanded( flex: 1, child: Container(width: 80.0, height:80.0, color: Colors.blue,), ), Expanded( flex: 1, child: Container(width: 80.0, height:80.0, color: Colors.yellow,), ) ], ),复制代码
流式布局
- 上面提到在
Row
和Column
中, 如果子widget
超出屏幕范围,则会报溢出错误 - 这是因为
Row
默认只有一行,如果超出屏幕不会折行 - 我们把超出屏幕显示范围会自动折行的布局称为流式布局
Flutter
中通过Wrap
和Flow
来支持流式布局
Wrap
Wrap({ Key key, 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, Listchildren = const [],})复制代码
可以看到Wrap
中的很多属性和Row
中相同, 这里就不在赘述了, 这里主要看一下Wrap
中特有的属性
alignment
子Widget
在主轴上的对其方式
// 默认值this.alignment = WrapAlignment.start// 取值: start, end, center, spaceBetween, spaceAround, spaceEvenly复制代码
runAlignment
子Widget
在纵轴上的对其方式
// 默认值this.runAlignment = WrapAlignment.start// 取值: start, end, center, spaceBetween, spaceAround, spaceEvenly复制代码
spacing
主轴方向子widget
的间距: spacing: 10
runSpacing
纵轴方向子widget
的间距: runSpacing: 10
Flow
- 一般很少会使用
Flow
,因为其过于复杂,需要自己实现子widget
的位置转换,在很多场景下首先要考虑的是Wrap
是否满足需求 Flow
主要用于一些需要自定义布局的UI或性能要求较高(如动画中)的场景Flow
有如下优点:- 性能好:
Flow
是一个对child
尺寸以及位置调整非常高效的控件,Flow
用转换矩阵对child
进行位置调整的时候进行了优化 - 在
Flow
定位过后,如果child
的尺寸或者位置发生了变化,在FlowDelegate
中的paintChildren()
方法中调用context.paintChild
进行重绘,而context.paintChild
在重绘时使用了转换矩阵,并没有实际调整Widget
位置。 - 灵活: 由于我们需要自己实现
FlowDelegate
的paintChildren()
方法,所以我们需要自己计算每一个widget
的位置,因此,可以实现自定义布局。
- 性能好:
- 缺点:
- 使用复杂.
- 不能自适应子
widget
大小,必须通过指定父容器大小或重写FlowDelegate
的getSize
返回固定大小
- 下面是一个简单的示例代码:
class FlowWidget extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return Container( color: Colors.orange, child: Flow( delegate: ShowFlowDelegate(margin: EdgeInsets.all(10)), children:[ Container(width: 100.0, height:100.0, color: Colors.red), Container(width: 100.0, height:100.0, color: Colors.yellow), Container(width: 100.0, height:100.0, color: Colors.blue), Container(width: 100.0, height:100.0, color: Colors.cyan), Container(width: 100.0, height:100.0, color: Colors.pink) ], ), ); }}复制代码
实现一个继承自FlowDelegate
的类, 并重写响应的方法
class ShowFlowDelegate extends FlowDelegate { EdgeInsets margin =EdgeInsets.zero; ShowFlowDelegate({ this.margin}); @override void paintChildren(FlowPaintingContext context) { var x = margin.left; var y = margin.top; //计算每一个子widget的位置 for (int i = 0; i < context.childCount; i++) { var w = context.getChildSize(i).width + x + margin.right; if (w < context.size.width) { context.paintChild(i, transform: new Matrix4.translationValues( x, y, 0.0)); x = w + margin.left; } else { x = margin.left; y += context.getChildSize(i).height + margin.top + margin.bottom; //绘制子widget(有优化) context.paintChild(i, transform: new Matrix4.translationValues( x, y, 0.0)); x += context.getChildSize(i).width + margin.left + margin.right; } } } @override Size getSize(BoxConstraints constraints) { // 设置Flow的大小 return Size(double.infinity, 300); } @override bool shouldRepaint(FlowDelegate oldDelegate) { return oldDelegate !=this; }}复制代码
层叠布局
- 层叠布局和
Web
中的绝对定位、iOS
中的Frame
布局是相似的,子widget
可以根据到父容器四个角的位置来确定本身的位置 - 绝对定位允许子
widget
堆叠(按照代码中声明的顺序) Flutter
中使用Stack
和Positioned
来实现绝对定位,Stack
允许子widget
堆叠,而Positioned
可以给子widget
定位
Stack
Stack({ Key key, this.alignment = AlignmentDirectional.topStart, this.textDirection, this.fit = StackFit.loose, this.overflow = Overflow.clip, Listchildren = const [],})复制代码
alignment
决定子Widget
在Stack
中的定位
// 默认值this.alignment = AlignmentDirectional.topStart// 取值如下, start和end为水平方向, top和bottom是垂直方向static const AlignmentDirectional topStart = AlignmentDirectional(-1.0, -1.0);static const AlignmentDirectional topCenter = AlignmentDirectional(0.0, -1.0);static const AlignmentDirectional topEnd = AlignmentDirectional(1.0, -1.0);static const AlignmentDirectional centerStart = AlignmentDirectional(-1.0, 0.0);static const AlignmentDirectional center = AlignmentDirectional(0.0, 0.0);static const AlignmentDirectional centerEnd = AlignmentDirectional(1.0, 0.0);static const AlignmentDirectional bottomStart = AlignmentDirectional(-1.0, 1.0);static const AlignmentDirectional bottomCenter = AlignmentDirectional(0.0, 1.0);static const AlignmentDirectional bottomEnd = AlignmentDirectional(1.0, 1.0);// 还可以使用具体数值比例定位, 设置值在0~1之间AlignmentDirectional(0.8, 0.9)复制代码
textDirection
决定alignment
对齐的参考系
// 默认ltrtextDirection: TextDirection.ltr// textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右// textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左复制代码
fit
用于决定没有定位的子widget
如何去适应Stack
的大小
// 默认值this.fit = StackFit.loose// StackFit.loose表示使用子widget的大小// StackFit.expand表示扩伸到Stack的大小复制代码
overflow
决定如何显示超出Stack
显示空间的子widget
// 默认值this.overflow = Overflow.clip// Overflow.clip时,超出部分会被剪裁(隐藏)// Overflow.visible时,时则不会被剪裁复制代码
使用示例
Stack( // alignment: AlignmentDirectional.center, alignment: AlignmentDirectional(0.8, 0.8), textDirection: TextDirection.ltr, fit: StackFit.loose, overflow: Overflow.visible, children:[ Container(width: 100.0, height:100.0, color: Colors.red), Container(width: 100.0, height:100.0, color: Colors.yellow), ],)复制代码
Positioned
Positioned
和iOS
中的Frame
设置位置和大小一样, 根据上下左右和宽高设置Widget
的定位和大小
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
用于指定定位元素的宽度和高度
注意,此处的width、height 和其它地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位widget,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理
child: Stack( alignment: AlignmentDirectional.center, children:[ // 这个widget会根据alignment的设置展示 Container(child: Text('https', style: TextStyle(color: Colors.red)), color: Colors.yellow,), // 这个widget会根据left和top和width的设置显示和alignment无关了, 实际width为80 Positioned( left: 10, top: 30, width: 80, child: Container(width: 100.0, height:100.0, color: Colors.red), ), Positioned( right: 10, bottom: 50, child: Container(width: 100.0, height:100.0, color: Colors.blue), ) ],),复制代码
至此,
Flutter
中布局相关的Widget
也都学习完了......接下来就是容器类Widget
了