相关博客系列文章: Flutter和Dart系列文章
相关Demo
地址: GitHub地址
布局类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
就是指直接或间接继承(包含)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
的主轴是垂直方向, 切两者的属性和使用都一样
相关下定义的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Row({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, List <Widget> children = const <Widget>[], }) Column({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline, List <Widget> children = const <Widget>[], })
相关属性如下 mainAxisAlignment
子Widget
在主轴方向的排列方式, 为方便以下皆称Widget
为组件
1 2 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
1 2 MainAxisSize mainAxisSize = MainAxisSize.max
表示Row
在主轴(水平)方向占用的空间,默认是MainAxisSize.max
max
表示尽可能多的占用水平方向的空间,此时无论子widgets
实际占用多少水平空间,Row
的宽度始终等于水平方向的最大宽度;
MainAxisSize.min
表示尽可能少的占用水平空间,当子widgets
没有占满水平剩余空间,则Row
的实际宽度等于所有子widgets
占用的的水平空间
verticalDirection
表示Row纵轴(垂直)的对齐方向, 默认值down
,表示从上到下; up
表示从下到上
1 2 VerticalDirection verticalDirection = VerticalDirection.down
crossAxisAlignment
表示子Widgets
在纵轴方向的对齐方式,Row
的高度等于子Widgets
中最高的子元素高度
crossAxisAlignment
的参考系是verticalDirection
1 2 3 4 5 6 7 8 CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center VerticalDirection.up时,crossAxisAlignment.start指底部对齐 /
当VerticalDirection.down
时, crossAxisAlignment
个枚举值如下
start
: 顶部对其
end
: 底部对其
center
: 居中对其
stretch
: 侧轴方向上, 子Widget
的高度拉伸至和Row
的高度相同
baseline
: 不论VerticalDirection
取值如何, 子Widget
的顶部和Row
的顶部对其
textDirection
表示水平方向子widget的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)
1 2 3 4 5 TextDirection textDirection rtl: 从右往左
textBaseline
用于对其文本的水平线, 详情可参考
1 2 3 4 5 TextBaseline textBaseline ideographic: 用于对齐表意基线
使用代码 1 2 3 4 5 6 7 8 9 10 11 12 13 Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, textDirection: TextDirection.ltr, verticalDirection: VerticalDirection.down, textBaseline: TextBaseline.ideographic, children: <Widget>[ 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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, List <Widget> children = const <Widget>[], }) Axis direction = Axis.horizontal Axis direction = Axis.vertical
Flex
继承自MultiChildRenderObjectWidget
,对应的RenderObject
为RenderFlex
,RenderFlex
中实现了其布局算法
Expanded 可以按比例缩放Row
、Column
和Flex
子widget
所占用的空间
1 2 3 4 5 6 7 8 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
的比例来分割主轴的全部空闲空间
1 2 3 4 5 6 7 8 9 10 11 12 13 Row( children: <Widget>[ 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 1 2 3 4 5 6 7 8 9 10 11 12 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, List <Widget> children = const <Widget>[], })
可以看到Wrap
中的很多属性和Row
中相同, 这里就不在赘述了, 这里主要看一下Wrap
中特有的属性
alignment 子Widget
在主轴上的对其方式
1 2 3 this .alignment = WrapAlignment.start
runAlignment 子Widget
在纵轴上的对其方式
1 2 3 this .runAlignment = WrapAlignment.start
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
返回固定大小
下面是一个简单的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class FlowWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.orange, child: Flow( delegate: ShowFlowDelegate(margin: EdgeInsets.all(10 )), children: <Widget>[ 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
的类, 并重写响应的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class ShowFlowDelegate extends FlowDelegate { EdgeInsets margin =EdgeInsets.zero; ShowFlowDelegate({this .margin}); @override void paintChildren(FlowPaintingContext context) { var x = margin.left; var y = margin.top; 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; 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) { 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 1 2 3 4 5 6 7 8 Stack({ Key key, this .alignment = AlignmentDirectional.topStart, this .textDirection, this .fit = StackFit.loose, this .overflow = Overflow.clip, List <Widget> children = const <Widget>[], })
alignment 决定子Widget
在Stack
中的定位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 this .alignment = AlignmentDirectional.topStartstatic 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 );AlignmentDirectional(0.8 , 0.9 )
textDirection 决定alignment
对齐的参考系
1 2 3 4 5 textDirection: TextDirection.ltr
fit 用于决定没有定位的子widget
如何去适应Stack
的大小
1 2 3 4 5 this .fit = StackFit.loose
overflow 决定如何显示超出Stack
显示空间的子widget
1 2 3 4 5 this .overflow = Overflow.clip
使用示例 1 2 3 4 5 6 7 8 9 10 11 Stack( alignment: AlignmentDirectional(0.8 , 0.8 ), textDirection: TextDirection.ltr, fit: StackFit.loose, overflow: Overflow.visible, children: <Widget>[ 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
的定位和大小
1 2 3 4 5 6 7 8 9 10 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),如果同时指定三个属性则会报错,垂直方向同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 child: Stack( alignment: AlignmentDirectional.center, children: <Widget>[ Container(child: Text('https' , style: TextStyle(color: Colors.red)), color: Colors.yellow,), 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
了
参考文献