布局类组件都会包含一个或多个组件,不同的布局类组件对子组件(layout)方式不同。在 Flutter 中 Element 树才是最终的绘制树,Element 树是通过 Widget 树来创建的 (通 Widget.createElement()) ,Widget 其实就是 Element 的配置数据。
在 Fluter 中,根据 Widget 是否需要包含子节点将 Widget 分为了三类,分别对应三种 Element,如下表:
Flutter 中很多 widget 都是继承自 StatelessWidget 或者 StatefulWidget ,然后再 build 方法中构建真正的 RenderObjectWidget。如 Text 是继承自 StatelessWidget ,然后在 build 方法中通过 RichText 构建子树,而 RichText 才是继承自 MultiChildRenderObjectWidget。
所以说 Text 属于 MultiChildRenderWidget(其他 Widget 也可以这样描述),其实 StatelessWidget 和 StatefulWidget 就是两个用于组合的 Widget 的基类,他们本身最终并不关联最终的渲染对象(RenderObjectWidget)
MultiChildRenderObjectWidget 是继承自 RenderObjectWidget 的,在 RenderObjectWidget 中定义了创建,更新 RenderObject 的方法,子类必须实现他们,其实 RenderObject 就是最终布局,渲染 UI 界面的对象,也就是说,对于布局类组件来说,其布局算法都是通过对应的 RenderObject 对象来实现的。
所以在 RichText 中就实现了 创建,更新 RenderObject 的方法
布局组件就是直接或间接继承(包含)MultiChildRenderObjectWidget 的 Widget,他们一般都会有一个 children 属性用于接收子 Widget 。
一个普通的 Widget 继承路线为:
继承 (Stateless/Stateful)Widget ,然后实现 build 方法
在 build 方法中通过创建继承自 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的类,然后实现对应的方法来构建最终的渲染UI界面的对象(RenderObject)
而 (Leaf/SingleChild/MultiChild)RenderObjectWidget 则是继承自 RenderObjectWidget ,最终继承自 Widget。
在 RenderObjectWidget 和 Widget 中定义着 创建,更新RenderObject 的方法。以及 createElement 方法。
其实 createElement 方法是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 类中实现的,而创建,更新 ObjectRender 则是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的实现类中完成的
线性布局(Row 和 Column)
线性布局指的是沿着水平或者垂直方向排布子组件。在 Flutter 中通过 Row 和 Column 来实现线性布局,类似于 Android 中的 LinearLayout 控件
Row 和 Column 都继承子 Flex,至于 Fiex 暂不多说
主轴和纵轴
在线性布局中,如果布局是水平方向,主轴就是指水平方向,纵轴即垂直方向;如果布局是垂直方向,主轴就是垂直方向,那么纵轴就是水平方向。
在线性布局中,有两个定义对齐方式的枚举类 MainAxisAlignment 和 CrossAxisAlignment ,分别代表主轴对齐和纵轴对齐
Row
Row 可以在水平方向排列子 Widget。其定义如下:
Row({ //...... MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisSize mainAxisSize = MainAxisSize.max, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, TextDirection? textDirection, VerticalDirection verticalDirection = VerticalDirection.down, TextBaseline textBaseline = TextBaseline.alphabetic, List<Widget> children = const <Widget>[], })
textDirection :水平方向组件的布局顺序,默认为系统当前 Locale 环境的文本方向(中文,英语都是左往右,而阿拉伯是右往左)
mainAxisSize:表示 Row 在主轴(水平)占用的空间,如 MainAxisSize.max 表示尽可能多的占用水平方向的空间,此时无论子 Widget 占用多少空间,Row 的宽度始终等于水平方向的最大宽度; MainAxisSize.min 表示尽可能的少占用水平空间,当子 Widget 没有占满水平剩余空间,则 Row 的实际宽度等于所有的子组件占用的水平空间。
其实就相当于 Android 中的 match_parent 和 warp_parent
mainAxisAlignment:表示子组件在 Row 所占水平空间的对齐方式,如果 mainAxisSize 值为 min,则此属性毫无意义,对应的值有 start ,center,end 等。
需要注意的是,textDirection 是 mainAxisAlignment 的参考系。例如 textDirection 是textDirection.ltr 时,则 MainAxisAlignment.start 表示左对齐,如果为 rtl 则,start 表示右对齐
crossAxisAlignment:表示子组件在纵轴的对齐方式,他的值也是 start,center,end 。只不过参考系是 verticalDirection 的值,具体的和上面的差不多,只是方向变了
children:子组件数组
栗子
class RowTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("线性布局 Row,Column"), ), body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [Text("Hello word"), Text("345")], ), Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [Text("Hello word"), Text("345")], ), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: [Text("Hello word"), Text("345")], ), Row( mainAxisAlignment: MainAxisAlignment.end, textDirection: TextDirection.rtl, children: [Text("Hello word"), Text("345")], ), Row( crossAxisAlignment: CrossAxisAlignment.start, verticalDirection: VerticalDirection.up, children: [ Text( "Hello word", style: TextStyle(fontSize: 30), ), Text("345") ], ) ], ) ); } }
Column
Column 可以在垂直方向排列其子组件,参数和 Row 一样,只不过排列的方式是垂直的,主轴和纵轴相反。
栗子
class ColumnTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("线性布局 Row,Column"), ), body: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [Text("Hi"), Text("World")], ), ); } }
由于有指定 主轴的 size,所以默认为 max。则这个 Column 会占用尽可能多的空间,这个栗子中为屏幕的高度
crossAxisAlignment 为 center,表示在纵轴上居中对齐。Colum 的宽度取决于其子 Widget 中宽度最大的 Widget,所以 hi 会被显示在 world 的中间部分
Row 和 Column 都只会在主轴上占用尽可能的最大空间,而纵轴的长度取决于他们最大子 Widget 的长度
如何让 hi 和 world 在屏幕中间对齐呢,有如下两种办法:
class ColumnTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("线性布局 Row,Column"), ), body: ConstrainedBox( constraints: BoxConstraints(minWidth: double.infinity), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [Text("Hi"), Text("World")], ), ), ); } }
特殊情况
如果 Row 嵌套 Row ,或者 Column 嵌套 Column,那么之后最外面的 Row/Column 会占用尽可能大的空间,
class ColumnTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("线性布局 Row,Column"), ), body: 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 "), ], ), ) ], ), ), )); } }
这种情况可以使用 Expanded 组件
children: <Widget>[ Expanded( child: Container( color: Colors.red, child: Column( mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度 children: <Widget>[ Text("hello world "), Text("I am Jack "), ], ), ), ) ]
弹性布局 Flex
弹性布局允许子组件按照一定比例来分配父容器空间。Flutter 中弹性布局主要通过 Flex 和 Expanded 来配合实现
Flex 组件可以沿着水平或者垂直方向排列子组件,如果知道主轴方向,使用 Row 或者 Column 会更方便一些。Row 和 Column 都继承子 Flex,参数也都基本相同,所以能使用 Flex 的地方基本上都可以使用 Row 或者 Column。
Flex 可以和 Expanded 组件配合实现弹性布局,大多数参数基本都和线性布局一样,这里不做介绍,定义如下
Flex({ Key? key, required this.direction, List<Widget> children = const <Widget>[], })
direction:弹性布局的方向,Row 默认为 水平方向,Column 默认为垂直方向
Flex 继承自 MultiChildRenderObjectWidget ,对应的 RenderObject 为 RenderFlex,RenderFlex 中实现了其布局算法
Expanded
可以按比例 扩伸 Row,Column 和 Flex 子组件所占用的空间
const Expanded({ int flex = 1, @required Widget child, })
flex:弹性系数,如果为 0 或者 null,则没有弹性,既不会扩展占用的空间。如果大于 0,所有的 Expanded 按照 flex 的比例来分隔主轴的全部空闲空间
栗子
class FlexTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("弹性布局Flex"), ), body: Column( children: [ Flex( direction: Axis.horizontal, children: [ Expanded( flex: 1, child: Container( height: 30, color: Colors.red, ), ), Expanded( flex: 2, child: Container( height: 30, color: Colors.blue, ), ) ], ), Padding( padding: const EdgeInsets.only(top: 20), child: SizedBox( height: 100, child: Flex( direction: Axis.vertical, children: [ Expanded( flex: 2, child: Container( height: 30, color: Colors.yellow, ), ), Spacer( flex: 1, ), Expanded( flex: 1, child: Container( height: 30, color: Colors.green, ), ) ], ), ), ) ], ), ); } }
栗子中的 Spacer 的功能是占用指定比例的空间,实际上它只是 Expanded 的一个包装类
流式布局 Wrap ,Flow
在使用 Row 和 Column 时,如果子 Widget 超出 屏幕范围,则会报溢出错误,如:
class WrapAndFlowTest extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("WarpAndFlow"), ), body: Container( height: 100, child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [Text("345" * 100)], ), ), ); } }
可以看到,右边部分报出溢出错误。这是因为 Row 默认只有一行,如果超出屏幕,不会折行,并且会报错
我们把超出自动折行的布局称为流式布局。Flutter 中通过 Wrap 和 Flow 来支持流式布局。
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>[], })
可以看到有很多属性在 Row ,Colum 中都有,如 direction,textDirection等,这些参数意义都相同,这里不过多介绍
spacing:主轴方向子 Widget 的间距
runSpacing:纵轴方向的间距
runAlignment:纵轴方向的对齐方式
例子
class WrapAndFlowTest extends StatelessWidget { final List<String> _list = const [ "爱是你我", "一壶老酒", "最炫民族风", "怒放的生命", "再见青春", "北京,北京" ]; List<Widget> getMusicList() { /* List<Widget> widgets = new List(); _list.forEach((element) { widgets.add(RaisedButton( child: Text(element), onPressed: () => print(element), )); });*/ return _list .map((e) => RaisedButton( child: Text(e), onPressed: () => print(e), )) .toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("WarpAndFlow"), ), body: Padding( padding: EdgeInsets.all(10), child: Flex( direction: Axis.horizontal, children: [ Expanded( flex: 1, child: Wrap( spacing: 25, runSpacing: 4, alignment: WrapAlignment.center, children: getMusicList(), ), ) ], ), )); } }