前言
记得iOS的布局有相对布局和自动布局,开发的时候可以采用frame的相对布局和Masonry自动布局,但不管什么的布局,都是以视图为一个个对象进行的。不过它并不像Flutter这样的,采用的包裹嵌套式布局,可以理解成’从整体到局部’,采用的是整体的方向加控件的嵌套约束,以达到控件的灵活布局的目的。
布局原理
向下传递约束
Flutter的视图结构类似‘二叉树’,当然常常也被开发者调侃成‘套娃’,那么就形成了类似线结构的。
Flutter和iOS的根结点对下一级的约束都是自身的frame,可以理解成紧约束。当然与之对应的就是松约束,在Flutter中可以Align定义的控件。
Container(
child: Text(),
),
Align(
child: Text(),
),
可以看到Container定义的是紧约束,Center定义的是松约束。当然如果你感兴趣的话,可以查看两者的继承层级看看为什么是这种效果。
主要是Align中的定义:
const Align({
Key? key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget? child,
}) : assert(alignment != null),
assert(widthFactor == null || widthFactor >= 0.0),
assert(heightFactor == null || heightFactor >= 0.0),
super(key: key, child: child);
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
可以看到RenderPositionedBox
中用到了RenderBox
,笔者猜测Flutter在这儿就将自身的size不再是由父级去约束了。具体的可以看看这个专门讲解的文章—RenderBox.
向上传递尺寸
这儿可以这样去理解,自控件的size=父级约束+自身的size设置,当父级对自身的约束是松约束时,那么可以根据自己的需要设置自身的大小;另一种情况就是父级紧约束了,这时候就是‘身不由己’,那自身的大小只能是父级的自身大小了。
获取约束
当开发者需要动态的获取当前的约束时,Flutter也是提供了一个控件LayoutBuilder
,参数返回当前的约束对象。
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
}),
当然LayoutBuilder
也受到父级的约束。
那么怎么去设置父级给自己的是松约束了,除了上面的Align
,其实最常用的还是ConstrainedBox
。
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Container(),
)
ConstrainedBox(constraints: BoxConstraints(
minWidth: 100,
maxWidth: 200,
minHeight: 100,
maxHeight: 200
)
);
常见布局
Flutter中提供了常用的布局控件,这样开发者可以根据界面的整体,采用哪种布局。
Flex
flex布局笔者最开始是从css中了解到的,发现其中的column``row
布局是非常方便开发的,当然Flutter中也是大概这样用的。
Column
Column
控件主要用于界面中竖直排列的部分,它里面可以包含多个child,并且Column
对子控件约束是松约束,当然在主轴上默认的是占满整个父视图的给自己的约束高度。如果交叉轴的设置是crossAxisAlignment:CrossAxisAlignment.stretch
的话,这个时候Column
给子控件的横向约束就是紧约束了。
class Column extends Flex {
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>[],
}) : super(
children: children,
key: key,
direction: Axis.vertical,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
textBaseline: textBaseline,
);
}
当然了,主轴横向的布局的控件就是Row
,和上面的Column
相反,这里就不赘叙了。
Expanded
这个控件在Flutter用的也比较多,但是它的出场一般都是和别的控件一起出现的,Expanded
字面意思是可展开的,那么说明它的作用就是在父级控件中可以让子控件在某个方向做弹性的伸缩。
Flex控件中如果按照弹性的属性分类的话,就可以分为两类。
1.Container
、SizedBox
等不可伸缩的控件。
2.Expanded
、Flexible
等可以伸缩的控件。
class Expanded extends Flexible {
const Expanded({
Key? key,
int flex = 1,
required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}
Expanded(child:
Container(
width: 20,
height: 20,
)
)
可以看到fit: FlexFit.tight
,说明Expanded
对子控件的约束是紧约束,那么再来看看这样的场景代码:
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
height: 100,
),
Container(
height: 20,
),
Expanded(child: Container(
width: 20,
height: 30,
)),
Container(
height: 40,
),
SizedBox(
height: 20,
),
Expanded(child: Container(
width: 20,
height: 30,
)),
],
),
)
可以看到,Column
中包含了两类控件,这个时候Column
的布局原理是怎么样的呢?Column
的主轴默认对子控件的约束是松约束,那就是0<=h<=Infinity
,说明高度是无限的约束。包含两类控件:
1.会对不可弹性的控件进行布局,根据先约束后确定尺寸的原理,就可以确定不可弹性的控件在主轴的高度大小了。
2.这样根据Column
的约束确定size,就可以知道弹性控件总共可以占用的高度区间是多大的了,然后可以根据flex
的数值分配多大的空间了。
根据这个原理就能解释为什么在Column
中的ListView
需要用Expanded
包裹了,原因:ListView
的控件的高度也是0<=h<=Infinity
,而Column
给自己的布局约束也是0<=h<=Infinity
,这样就使得ListView
的高度就是无边界的,所以需要加一个Expanded
。
Stack
Stack
控件中包裹的子控件排列方向是按照屏幕的z轴,一般主要是满足一些视图的部分区域叠加。在css中一般是把视图属性设置成position: absolute
,这样子控件就被定义成了绝对布局了。
Stack(
children: [
Positioned(
child: Container(),
left: 0,
top: 0,
),
Positioned(
child: Container(),
left: 10,
top: 10,
)
],
)
Stack({
Key? key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
this.clipBehavior = Clip.hardEdge,
List<Widget> children = const <Widget>[],
}) : assert(clipBehavior != null),
super(key: key, children: children);
可以看到,Flutter也用到了position
类似的东西了。并且this.fit = StackFit.loose
说明该控件用到了松约束。
再来看看Stack
是怎么设置自己的size的呢?
Stack(
children: [
Positioned(
child: Container(),
left: 0,
top: 0,
),
Positioned(
child: Container(),
left: 10,
top: 10,
)
],
)
当Stack
中只有Positioned
包裹的子控件的时候,会发现Stack
的大小是父级的Frame,这样子控件通过Positioned
包裹才会让子控件更加满足开发者的意图。
Container(
color: Colors.red,
child: Stack(
children: [
Text(
'Hello',
style: TextStyle(fontSize: 20),
),
Text(
'Flutter',
style: TextStyle(fontSize: 30),
)
],
),
)
当`Stack`中只有普通控件的时候,会发现它的大小是无`Positioned`包裹控件中最大控件的尺寸。
Container(
color: Colors.red,
child: Stack(
children: [
Positioned(
child: Container(
color: Colors.green,
width: 200,
height: 200,
),
left: 0,
top: 0,
),
Text(
'Hello',
style: TextStyle(fontSize: 20),
),
Text(
'Flutter',
style: TextStyle(fontSize: 30),
)
],
),
)
当`Stack`中两类控件都有的时候:<br />1.确定普通控件的大小,并且把其中最大的`size`设置成`Stack`的大小。<br />2.布局`Positioned`的控件,根据其属性布局。<br />这里会涉及到一种情况,当`Positioned`的控件超出了`Stack`控件的区域时,点击超出的区域时,是不响应用户的点击事件,当然这里面涉及到了Flutter的事件机制,之后会专门有一篇文章写到。
总结
这里只是说到了Flutter开发过程中用的比较多的布局,以及介绍了它们各自的布局原理,当然这些布局控件用起来到比较容易,这里只是介绍我们在用的过程中,可能对这个控件自身的布局原理不是很了解,所以笔者做一个流程上的说明。