主要学习布局类组件
- 线性布局(Row、Column)
- 弹性布局(Flex)
- 流式布局(Wrap、Flow)
- 层叠布局(Stack、Positioned)
- 对齐与相对定位(Align)
布局类组件简介
布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排版layout方式不同。 之前说ELement树才是最终的绘制树,Element树是通过Widget树来创建的(通过Widget.createElement()),Widget就是Element的配置数据。 在Flutter中,根据Widget是否包含子节点将Widget分为三类,分别对应三种Element,如下:
LeafRenderObjectWidget
- 对应的Element:LeafRenderObjectElement
- 用途:Widget树的叶子节点,用于没有子节点的Widget,通常基础组件都属于这一类,如Image
SingleChildRenderObjectWidget
- SingleChildRenderObjectElement
- 包含一个子Widget,如:ConstrainedBox、DecorateBox等
MultiChildRenderObjectWidget
- MultiChildRenderObjectElement
- 包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等。
Flutter中的很多Widget是直接继承自StatelessWidget或StatefulWidget,然后再build()方法中构建真正的RenderObjectWidget。 如,Text,继承自StatelessWidget,然后在build()方法中通过RichText来构建其子树,而RichText才是继承自MultiChildRenderObjectWidget。所以为了方便叙述,我们也可以直接说Text属于MultiChildRenderObjectWidget(其它widget也可以这么描述),这才是本质。 其实StatefulWidget和StatelessWidget就是两个用于组合Widget的基类,它们本身并不关联最终的渲染对象(RenderObjectWidget)。
布局类组件就是指直接或间接继承(包含)MulitiChildRenderObjectWidget的Widget,一般都会有一个children属性用于接收子Widget。
继承关系: Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget
RenderObjectWidget类中定义了创建、更新RenderObject的方法,子类必须实现他们,关于RenderObject我们现在只需要知道它是最终布局、渲染UI界面的对象即可,也就是说,对于布局类组件来说,其布局算法都是通过对应的RenderObject对象来实现的,所以读者如果对接下来介绍的某个布局类组件的原理感兴趣,可以查看其对应的RenderObject的实现,比如Stack(层叠布局)对应的RenderObject对象就是RenderStack,而层叠布局的实现就在RenderStack中。
线性布局(Row、Column)
线性布局就是指沿水平或垂直方向排布子组件。Flutter中通过Row和Column来实现线性布局类似于ANdroid中的LinearLayout控件。 Row和Column继承自Flex
主轴和纵轴
对于线性布局,若布局是沿水平方向,那么主轴就是指水平方向,而纵轴即垂直方向。如果布局沿垂直方向,那么主轴就是指垂直方向,而纵轴就是水平方向。
在线性布局中,有2个定义对齐方式的枚举类MainAxisAlignment和CrossAxisAlignment分别代表主轴对齐和纵轴对齐。
Row
- Row可以在水平方向排列其子widget,定义如下:
Row({
...
TextDirection textDirection, // 表示水平方向子组件的布局顺序(从左往右还是从右往左)
MainAxisSize mainAxisSize = MainAxisSize.max, // 表示Row在主轴(水平)方向占用的空间,默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的的水平空间;
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 表示子组件在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 verticalDirection = VerticalDirection.down, // 表示Row纵轴(垂直)的对齐方向,默认是VerticalDirection.down,表示从上到下。
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 表示子组件在纵轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.down时crossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.end和crossAxisAlignment.start正好相反;
List<Widget> children = const <Widget>[], // 子组件数组
})
示例
Column(
// 测试Row对齐方式,排除Column默认居中对齐的干扰
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row( // 默认居中对齐
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
],
),
Row( // 由于mainAxisSize的值为MainAxisSize.min,Row的宽度等于两个Text的宽度和,所以对齐没得意义,从左往右排
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
],
),
Row( // Row设置textDirection值为TextDirection.rtl,所以子组件会从右向左的顺序排列,而此时MainAxisAlignment.end表示左对齐,
mainAxisAlignment: MainAxisAlignment.end,
textDirection: TextDirection.rtl,
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
],
),
Row( // 测试纵轴的对齐方式,由于2个子Text字体不一样,所以搞得不一样,指定verticalDirection值为VerticalDirection.up,即从低向顶排列。而此时crossAxisAlignment值为CrossAxisAlignment.start表示底对齐。
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: verticalDirection.up,
children: <Widget>[
Text("hello world", style: TextStyle(foontSize: 30.0),),
Text("I am Jack"),
],
),
],
);
Column
Column可以在垂直方向排列子组件。参数和Row一样。
示例:
import 'package:flutter/material.dart';
class CenterColumnRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
chilren: <Widget>[
Text("hi"),
Text("World"),
]
)
}
}
/*
由于没有指定Column的mainAxis,所以使用默认值MainAxisSize.max,则Column会在垂直方向占用尽可能多的空间,此例中为屏幕高度。
由于我们指定了 crossAxisAlignment 属性为CrossAxisAlignment.center,那么子项在Column纵轴方向(此时为水平方向)会居中对齐。注意,在水平方向对齐是有边界的,总宽度为Column占用空间的实际宽度,而实际的宽度取决于子项中宽度最大的Widget。在本例中,Column有两个子Widget,而显示“world”的Text宽度最大,所以Column的实际宽度则为Text("world") 的宽度,所以居中对齐后Text("hi")会显示在Text("world")的中间部分
*/
特殊情况
- 若Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column会占用尽可能大的空间,里面Row和Column所占空间为实际大小
container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, // 有效,外层Colunm为整个屏幕
children: <Widget>[
Container(
color: Coloes.red,
child: Column(
mainAxisSize: MainAxisSize.max, // 无效,内层Column为实际高度
children: <Widget>[
Text("Hellow world"),
Text("I am Jack"),
],
),
)
],
),
),
);
- 若要让里面的Column占满外部Column,可以使用Expanded组件:
Expanded(
child: Container(
color: Colors.red,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 垂直方向居中对齐
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
],
),
),
)
弹性布局(Flex)
- 弹性布局允许子组件按照一定比例来分配父容器空间。
- 类似于H5的弹性盒子布局
- Flutter中的弹性布局主要通过Flex和Expanded来配合实现
Flex
- Flex组件可以沿水平或者垂直方向排列子组件,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。
- Flex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。
Flex({
...
this.direction, // 弹性布局的方向,Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <widget>[],
...
})
- Flex继承自MultiChildRenderObjectWidget,对应的RenderObject为RenderFlex,RenderFlex中实现了其布局算法
EXpanded
- 可以按比例“扩伸”Row,Column,Flex子组件所占的空间
const Expanded({
int flex =1, // 弹性系数,若为0,或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。
Widget child,
})
- 示例
class FlexLayoutTestRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
chilren:<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(
hieght: 30.0,
color: Colors.green,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: SizeBox(
height: 100.0,
//Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间
child: Flex(
direction: Axis.vertical,
chilren: <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,
),
)
]
)
)
)
]
)
}
}
Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类。
源码如下:
class Spacer extends StatelessWidget {
const Spacer({Key key, this.flex = 1})
: assert(flex != null),
assert(flex > 0),
super(key: key);
final int flex;
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
}
流式布局
当使用Row或Column时,若widget超出屏幕范围,则会报溢出错误
Flutter中通过Wrap和Flow来支持流式布局
Wrap
- 之定义:
Wrap({
...
this.direction = Axis.horizontal, // 方向
this.alignment = WrapAlignment.start, // 对齐方式
this.spacing = 0.0, // 主轴方向子widget的间距
this.runAlignment = WrapAlignment.start,// 纵轴方向的对齐方式
this.runSpacing = 0.0, // 纵轴方向的间距
this.crossAxisAlignment = WrapCrossAlignment.start,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
List<Widget> children = const <Widget>[],
})
- demo
Wrap(
spacing: 8.0, // 主轴(水平方向间距)
runSpacing: 4.0, // 纵轴(垂直方向间距)
alignment: WrapAlignment.center, // 沿主轴方向居中
children: <Widget> [
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
label: new Text('Hamilton'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: new Text('Lafayette'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
label: new Text('Mulligan'),
),
new Chip(
avatar: new CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
label: new Text('Laurens'),
),
]
)
Flow
- 很少用FLow,过于复杂
层叠布局Stack、Positioned
- 层叠布局和Web中的绝对定位、Android中的Frame布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。
- 绝对定位允许子组件堆叠起来(按照代码中声明的顺序)。
- Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位。
- Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。
Stack
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
Positioned
const Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
Widget child,
})
对齐与相对定位(Align)
- 通过Stack和Positioned,我们可以指定一个或多个子元素相对于父元素各个边的精确偏移,并且可以重叠。
- 但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。