一、初识布局
1.1 约束(Terminology)的向下传递
Flutter从一个runApp函数开始运行,例如:
void main() {
runApp(//1.向下Container传递紧约束:屏幕尺寸
Container(//2.向下Center传递约束,由于自身的w和h无效,因此原封不动地传递
width: 0.04,//1.来自App的约束使width失效,w=页面宽度
height: 0.08,//1.来自App的约束使height失效,h=页面高度
color: Colors.red[200],
child: Center(//3.向下Container传递松约束,下限设为0
child: Container(//4.向下FlutterLogo传递紧约束:200<=h,w<=200
color: Colors.white,
width: 200,//3.满足Center的约束,w=200
height: 200,//3.满足Center的约束,h=200
child: FlutterLogo(
size: 10//不满足约束条件,失效,size=200
),
),
)
));
}
上面这段代码的运行结果如下图:
通过读取LayoutBuilder
的constrants参数,我们可以得到来自父组件的约束区间:
LayoutBuilder(builder: (context, constrants) {
print("constrants: $constrants");
return FlutterLogo(size: 10);
}),
我们可以通过Align
系列控件来放松约束,例如使用Center
。
1.2 SizedBox和FractionallySizedBox
我们可以使用SizedBox
和FractionallySizedBox
来确定组件的大小,其中后者使用一个比例关系来约束。
但如果SizedBox
本身受到一个紧约束,那么它也无法突破。
目前我们只能使用Align
系列Widgets(比如Center
)来将紧约束转换为松约束,例如将200<=h,w<=200
转化为0<=h,w<=200
。
Container(
//向下SizedBox传递紧约束:200<=h,w<=200
width: 200,
height: 200,
child: SizedBox( //收到Container的紧约束
height: 100,//失效
width: 100,//失效
child: FlutterLogo(size: 10), //不满足约束条件,失效,size=200
)
1.3 ConstrainedBox
Container( //向下传递紧约束:h,w=200
color: Colors.black,
width: 200,
height: 500,
child: Center(//向下传递松约束:0<=h,w<=200
child: ConstrainedBox(//重新定义约束区间(同样,收到上级约束的限制)
constraints: const BoxConstraints(
minWidth: 60,
maxWidth: double.infinity,//无限大,被上级限制为200
minHeight: 60,
maxHeight: 120,
),
child: LayoutBuilder(builder: (context, constraints) {
print("$constraints");
//flutter: BoxConstraints(60.0<=w<=120.0, 60.0<=h<=120.0)
return const FlutterLogo(size: 150);//150被限制回120
}),
),
),
),
ConstrainedBox
可以重新定义约束区间,但新定义的约束区间必须在父约束之内。我们可以使用loosen()
函数取消minWidth
和minHeight
,也就是将它们设置为0.
Container( //向下传递紧约束:h,w=200
color: Colors.black,
width: 200,
height: 500,
child: Center(//向下传递松约束:0<=h,w<=200
child: ConstrainedBox(//重新定义约束区间(同样,收到上级约束的限制)
constraints: const BoxConstraints(
minWidth: 60,
maxWidth: double.infinity,//无限大,被上级限制为200
minHeight: 60,
maxHeight: 120,
).loosen(), //变为松约束(下限为0)
child: LayoutBuilder(builder: (context, constraints) {
print("$constraints");
//flutter: BoxConstraints(0.0<=w<=120.0, 0.0<=h<=120.0)
return const FlutterLogo(size: 150);//150被限制回120
}),
),
),
),
- 紧约束:最大值等于最小值的约束;
- 松约束:最小值等于0的约束;
- 既是松约束,又是紧约束的约束:最大值与最小值都是0的约束。
同时,
- 有界约束:有设置最大值的约束;
- 无界约束:最大值是无限大的约束,例如无限长的ListView。
1.4 Padding
Padding组件可以收缩Container的紧约束,但仍向下传递一个紧约束。
Container(
color: Colors.black,
width: 300,
height: 300,
child: Padding(
padding: const EdgeInsets.all(20.0),
//上下同时设置20的边框,收缩了Container的紧约束
child: LayoutBuilder(builder: (context, constraints) {
print("$constraints");
//BoxConstraints(w=260.0, h=260.0),这里仍然是一个紧约束
return const FlutterLogo(size: 150);//150仍然失效
}),
),
),
二、弹性布局——Column和Flexible
2.1 Column和Directionality
Scaffold(
body: Center(
child: Container(
color: Colors.red[200],
width: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
//主轴环绕留白
crossAxisAlignment: CrossAxisAlignment.start,
//交叉轴:从起始处(一般从左到右)
//某些语言的阅读顺序是从右向左,可以使用Directionality调整
children: const [
FlutterLogo(size:150),
FlutterLogo(size: 50),
FlutterLogo(size: 150),
],)),
),
)
Column还有一些其他属性,如mainAxisSize
和CrossAxisAlignment.stretch
:
Scaffold(
body: Center(
child: Container(
color: Colors.red[200],
child: Column(
mainAxisSize: MainAxisSize.min,//设置主轴方向的长度,默认max
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
//交叉轴:拉伸至与父限制一致(类型交叉轴的expanded)
children: const [
FlutterLogo(size:50),
FlutterLogo(size: 50),
FlutterLogo(size: 50),
],)),
),
)
2.2 Expanded
使用Expanded
组件尽可能多地填满剩余空间:
Center(
child: Container(
color: Colors.red[200],
child: Column(
//交叉轴:拉伸至与父限制一致(类型交叉轴的expand)
children: [
FlutterLogo(size:50),
FlutterLogo(size: 50),
Expanded(child: Container(color: Colors.black)),
FlutterLogo(size: 50),
],)),
),
通过指定flex
参数设置两个Expanded
之间的比例:
Center(
child: Container(
color: Colors.red[200],
child: Column(
//交叉轴:拉伸至与父限制一致(类型交叉轴的expand)
children: [
FlutterLogo(size:50),
Expanded(flex:1, child: Container(color: Colors.green)),
FlutterLogo(size: 50),
Expanded(flex:2, child: Container(color: Colors.green)),
FlutterLogo(size: 50),
],)),
)
Expanded
组件的本质是Flexible
组件,例如:
Expanded(
child: Container(color: Colors.green)
),
//等价于
Flexible(
flex:1,
child: Container(color: Colors.green)
),
-
使用Flexible时,Flutter首先会渲染Column中固定的项目,再用剩余的空间分比例占取。
-
Flutter在布局其中的非弹性控件时(例如FlutterLogo),会先假装自己是一个无界约束,例如
Column( children: [ LayoutBuilder(builder: (_, con) { print('$con'); //flutter: BoxConstraints(0.0<=w<=375.0, 0.0<=h<=Infinity) return FlutterLogo(size: 50); }), Expanded(flex: 1, child: Container(color: Colors.green)), FlutterLogo(size: 50), Expanded(flex: 2, child: Container(color: Colors.green)), FlutterLogo(size: 50), ], )
这是由于Column没有办法将自己受到的限制提前告知FlutterLogo,为了尽可能地不限制FlutterLogo,Column会传入一个Infinity“假装”自己是一个无界控件,但如果真的超出了限制,则会提示错误。
2.3 Column中的ListView
倘若我们直接在Column中插入ListView,则会提示错误:
Column(
children: [
FlutterLogo(size: 50),
ListView(children: [ //报错!
for(int i=0;i<10;i++) Text("ListView item $i")
]),
FlutterLogo(size: 50),
],
)
正如我们上面提到的那样,Column在渲染时会假装自己是一个无界控件。Column的渲染分成两个阶段:
-
渲染固定组件(非
Flexible
组件) -
将剩余的空间分配给children中的各个弹性(也就是
Flexible
)组件
而在上面的代码中,ListView
不是一个Flexible组件,所以它参与第一个阶段的渲染,它发现自己受到的约束是Infinity(请参考2.2节的最后一段代码)。而ListView本身也是一个Infinity组件,于是它就真的使用Infinity的空间,超出了Column实际受到的限制(比如:屏幕尺寸),于是就会产生内容溢出的错误。
因此,如果我们需要在Column中使用ListView,就需要给予ListView限制,可以用Expanded包裹ListView:
Column(
children: [
FlutterLogo(size: 50),
Expanded( //使用Expanded包裹ListView
child: ListView(children: [
for(int i=0;i<100;i++) Text("ListView item $i")
]),
),
FlutterLogo(size: 50),
],
)
三、层叠组件——Stack
3.1 Stack组件
用下面一段代码展示Stack的作用:
Center(
child: Container(
color: Colors.grey, //可以看到Stack本身的大小,受到children的影响
child: Stack(
alignment: Alignment(0,0),//等价于Alignment.center
children: [
FlutterLogo(size: 100),
Text("111",style: TextStyle(fontSize: 150)),
Positioned(
left: 0,
top: 0,
child: Container(color: Colors.red,width: 30,height: 30)
),
],),
),
)
3.2 Stack的渲染过程
与Column组件类似,Stack中的成员分成两类:有位置的(也就是Positioned
)和没有位置的。
在渲染的过程中Stack如何确定其自身的大小呢?
- Stack会首先将没有位置的组件全部渲染出来,并以此确定其自身的大小;
- 然后再按照位置渲染
Positioned
组件。
我们再看一个例子:
Container(
color: Colors.grey,
child: Stack(
alignment: Alignment(0,0),
children: [
Positioned( //Stack本身的大小与Positioned组件无关!
left: 0, bottom: 0,
child: FlutterLogo(size: 300)
),
Text("111",style: TextStyle(fontSize: 150)),
Positioned(
left: 0,
top: 0,
child: Container(color: Colors.red,width: 30,height: 30)
),
],),
)
也因此,FlutterLogo没有被完整渲染出来:
我们可以指定fit属性
来将父组件的约束传给下级组件。不是让Stack
本身扩展,而是让其中的children
受到Stack
上级组件的约束,Stack
一般也会受到影响,但那只是一个副作用:
Stack(
fit: StackFit.expand,
...
),
Container(
constraints: BoxConstraints(
minWidth:30,
),
child: Stack(
fit: StackFit.passthrough,
//直接将上级的非紧约束传给children
)
)
3.3 Positioned组件
首先,Positioned会强行要求其中的位置信息达成,也就是间接地确定了其中控件的大小:
Container(
color: Colors.grey,
child: Stack(
alignment: Alignment(0,0),
children: [
Positioned(
left: 0, //假如我们既设置了left,又设置了right,会怎么样呢?
right: 0,
child: Container(color: Colors.blue, width: 30, height: 30)
//答案是:Container本身的w和h将会失效!
),
Text("111",style: TextStyle(fontSize: 150)),
Positioned(
left: 0,
top: 0,
child: Container(color: Colors.red,width: 30,height: 30)
),
],),
),
另外,如果我们定义了Positioned,但却没有给它传递任何信息,那么Stack就会将Positioned忽略,此时Positioned里面的组件将会参与第一阶段的渲染,也就是会影响Stack本身的大小。
我们可以通过指定clipBehavior
属性来确定超出部分是否会被裁减:
Container(
color: Colors.grey, //可以看到Stack本身的大小,受到children的影响
child: Stack(
clipBehavior: Clip.none,//默认Clip.hardEdge
//等价于旧版本的overflow: Overflow.visible,
alignment: Alignment(0,0),//等价于Alignment.center
children: [
Positioned(
left: 0,
width: 300, //溢出边框啦!
child: Container(color: Colors.blue, width: 30, height: 30)
),
Text("111",style: TextStyle(fontSize: 150)),
Positioned(
left: 0,
top: 0,
child: Container(color: Colors.red,width: 30,height: 30)
),
],),
),
需要注意的是,如果控件的尺寸超出了Stack本身的尺寸,那么在Stack外的点击事件将不会生效。因为那里的点击事件不会发送给Stack,那么也就不会传送给Stack的children。
另外,即使设置了Clip.hardEdge
,对于Transform指定的变换位移,超出部分依然不会被裁减,这也是因为Flutter的优化机制导致的。
四、再谈Container
4.1 Container语法糖
一般来说,Container会传递一个紧约束,我们可以通过Align系列组件放松约束:
//很繁琐
Container(
color: Colors.orange,
width: 200,
height: 200,
child: const Padding(
padding: EdgeInsets.all(24.0),
child: Align(
alignment: Alignment.topLeft,
child: FlutterLogo(),
),
),
)
这样做实在太繁琐,我们可以直接使用Container中的padding
和alignment
属性,这是Flutter提供的语法糖:
//与上面的代码等价
Container(
color: Colors.orange,
width: 200,
height: 200,
padding: const EdgeInsets.all(24.0),
alignment: Alignment.topLeft,
child: const FlutterLogo(),
)
另外,还有一些其他的语法糖,例如color
、decoration
、foregroundDecoration
、constraints
对应ColoredBox
、DecoratedBox
、ConstrainedBox
,height/width
对应SizedBox
。
- color => ColoredBox
- decoration / foregroundDecoration => DecoratedBox
- constraints => ConstrainedBox
- clipBehavior => Clip
- height/width => SizedBox
- margin => Padding 添加在Container外
- padding => Padding 添加在child外,Container内
- transform => Transform组件
4.2 Container行为
一般来说,如果给Container指定一个child,那么Container的大小就会与其child一致,除非您设置了alignment属性。否则就会直接占满父组件。
而如果Container在诸如Column这样主轴无界的组件内,那么它的该轴上的长度会被调整为0。
使用margin属性时也会影响Container的大小。
Container
如何知道自己所在的组件是否有界呢?这里可以参考LimitedBox
组件,当出现某个尺寸无穷大的情况,会自动缩减成maxHeight
,而如果不是infinity的情况,该轴的尺寸则可以超过maxHeight
,也就是LimitedBox
此时无任何效果,因此,这里的maxHeight
其实可以等同于Placeholder
中的fallbackHeight
属性。
更多信息,可以参考Container的源代码。
五、CustomMultiChildLayout
5.1 制作一个自己的Stack
我们可以使用CustomMultiChildLayout
组件自制一个简易的Stack:
CustomMultiChildLayout(
delegate: MyDelegate(),
children: [
//标记其中的组件,使得MyDelegate中可以找到这些组件
//LayoutID与Positioned类似,只能在这里使用
//* 这里的Z-Order由children顺序决定,与MyDelegate无关!
LayoutId(id: 1, child: FlutterLogo()),
LayoutId(id: 2, child: FlutterLogo()),
],
)
自定义一个MyDelegate类作为代理:
class MyDelegate extends MultiChildLayoutDelegate {
@override
void performLayout(Size size) {
if (hasChild(1)) {
final size1 = layoutChild(
1, //1号组件layout,并得到它的size(会想:向下传递限制,向上传递大小)
BoxConstraints.tight(const Size(100, 100)) //向下传递限制
);
positionChild(1, Offset.zero); //将1号组件放在左上角
}
if (hasChild(2)) {
final size2 = layoutChild(
2, //2号组件layout,并得到它的size(会想:向下传递限制,向上传递大小)
BoxConstraints.tight(const Size(200, 200)) //向下传递限制
);
positionChild(2, Offset.zero); //将2号组件放在左上角
}
}
//返回true表示永远需要重新更新,这个函数用作优化,这里暂不考虑
@override
bool shouldRelayout(_) => true;
}
positionChild(2, Offset(0,size1.height)); //将2号组件放在1号组件下面:类似Column
同样,我们可以设置BoxConstraints.loost(size)
来传递松约束:
void performLayout(Size size) {
if (hasChild(1)) {
final size1 = layoutChild(
1, //1号组件layout,并得到它的size(会想:向下传递限制,向上传递大小)
BoxConstraints.loost(size) //向下传递限制
);
positionChild(1, Offset.zero); //将1号组件放在左上角
}
if (hasChild(2)) {
final size2 = layoutChild(
2, //2号组件layout,并得到它的size(会想:向下传递限制,向上传递大小)
BoxConstraints.tight(const Size(200, 200)) //向下传递限制
);
positionChild(2, Offset.zero); //将2号组件放在左上角
}
}
5.3 MyDelegate的局限性
- 首先,MyDelegate无法在绘制时获得其子组件的大小。通过继承
MultiChildLayoutDelegate
的Size getSize(BoxConstraints c)
方法,我们可以得到上级组件给我们的限制,也可以向上传递我们需要的Size大小,但我们并不能通过这个函数获得子组件的大小,也就是说我们无法使得MyDelegate自适应子组件的大小。 - 我们无法在
performLayout
函数中“无中生有”,我们只能在其中画出children数组中已经存在的组件,不能在这里创造出一个新的组件。
为了解决上述两个问题,我们可以使用RenderObject
六、布局原理——RenderObject
6.1 简述
我们知道,平常我们在写的Stateless、Stateful组件,其实并没有将自己画在屏幕上的能力,它们只是定义了一个build方法而已。真正将内容写在屏幕上的,其实是RenderObject
。
我们举一个例子来说明:
Container(
color: Colors.grey,
child: MyRenderBox(
child: const FlutterLogo(size: 200),
),
),
class MyRenderBox extends SingleChildRenderObjectWidget {
MyRenderBox({required Widget child}) : super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderMyRenderBox();
}
}
class RenderMyRenderBox extends RenderBox with RenderObjectWithChildMixin {
//渲染pipeline:performLayout、paint、composition
@override
void performLayout() {
//处理布局
child!.layout(constraints, //也可以自定义紧约束
parentUsesSize: true); //作为父组件需要使用子组件的size,默认是false
//优化用:如果不需要,子组件改动时就可以不必继续遍历渲染父组件
//size = const Size(300, 300);
size = (child as RenderBox).size;
}
@override
void paint(PaintingContext context, Offset offset) {
//处理绘制
context.paintChild(child!, offset + Offset(120,120));
//FlutterLogo跑到MyRenderBox外面去了!
//context.canvas.draw** //可以在这里画任意图形!
//context.pushOpacity //Opacity原理:使用push增加一个图层(资源耗费多)
}
}
在上述代码中,performLayout和paint是两次不同的遍历过程(flutter一般是深度优先),因此这里的FlutterLogo可以跑到MyRenderBox外面去!
paintChild函数是非常自由的,我们可以选择canvas.draw或者push系列函数做更多想要的事情,也可以干脆不做任何事。
6.2 Hot Reload原理——updateRenderObject
通过自制一个ShadowRenderBox,我们可以实现阴影效果:
Container(
color: Colors.grey,
child: ShadowBox(
distance: 20.0,
child: const Icon(Icons.category, size: 80),
),
),
class ShadowBox extends SingleChildRenderObjectWidget {
final double distance;
ShadowBox({required Widget child, required this.distance})
: super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderShadowBox(distance: distance);
}
@override
void updateRenderObject(
BuildContext context, covariant RenderShadowBox renderObject) {
//如果不定义这个函数,热重载时会根据key和类型判断元素是否改变,如果没有改变,则继续使用原来的renderObject,distance的值不会改变
renderObject.distance = distance;
}
}
/*
也可以使用 class RenderShadowBox extends RenderProxyBox
此时,不再performLayout()和paint函数不再必须,只需要写我们需要的那个函数即可
*/
class RenderShadowBox extends RenderBox with RenderObjectWithChildMixin {
double distance;
RenderShadowBox({required this.distance});
@override
void performLayout() {
//处理布局
child!.layout(constraints, parentUsesSize: true);
size = (child as RenderBox).size;
}
@override
void paint(PaintingContext context, Offset offset) {
context.paintChild(child!, offset);
//插入一个新的半透明图层
context.pushOpacity(offset, 127, (context, offset) {
context.paintChild(child!, offset + Offset(distance, distance));
});
}
}
上面的RenderShadowBox
可以简单地写成下面的形式:
class RenderShadowBox extends RenderProxyBox {
double distance;
RenderShadowBox({required this.distance});
@override
void paint(PaintingContext context, Offset offset) {
context.paintChild(child!, offset);
context.pushOpacity(offset, 127, (context, offset) {
context.paintChild(child!, offset + Offset(distance, distance));
});
}
}
另外,也可以设置DebugOverflowIndicatorMixin
来定义超出界限后的那个黑黄相间的错误框。那个overflow组件也只是通过paintOverflowIndicator()
手动绘制的而已。