Flutter布局详解,必知必会

本文的目的是为了让读者掌握不同布局类Widget的布局特点,分享一些在实际使用过程遇到的一些问题,在《Flutter实战》这本书中已经讲解的很详细了,本文主要是对其内容的浓缩及实际遇到的问题的补充。

什么是布局类Widget

布局类Widget就是指直接或间接继承(包含)MultiChildRenderObjectWidget的Widget,它们一般都会有一个children属性用于接收子Widget。在Flutter中Element树才是最终的绘制树,Element树是通过widget树来创建的(通过Widget.createElement()),widget其实就是Element的配置数据。它的最终布局、UI界面渲染都是通过RenderObject对象来实现的,这里的细节我就不详细描述了,因为我也不懂。不过感兴趣的小伙伴也可以看看本专栏的Flutter视图的Layout与Paint这篇文章。

Flutter中主要有以下几种布局类的Widget:

  • 线性布局Row和Column
  • 弹性布局Flex
  • 流式布局Wrap、Flow
  • 层叠布局Stack、Positioned

本文[Demo地址]( )

线性布局Row和Column

线性布局其实是指沿水平或垂直方向排布子Widget,Flutter中通过Row来实现水平方向的子Widegt布局,通过Column来实现垂直方向的子Widget布局。他们都继承Flex,所以它们有很多相似的属性。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在前端的Flex布局中,默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。与Flutter中MainAxisAlignment和CrossAxisAlignment类似,分别代表主轴对齐和纵轴对齐。

源码属性解读

Row({

MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List children = const [],
})

Column({

MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List children = const [],
})

  • textDirection:表示水平方向子widget的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。
  • 主轴方向: Row即为水平方向,Column为垂直方向
  • mainAxisAlignment 主轴方向,对child起作用
  • center:将children放置在主轴的中心
  • start:将children放置在主轴的起点
  • end:将children放置在主轴的末尾
  • spaceAround:将主轴方向上的空白区域均分,使children之间的空白区域相等,但是首尾child的靠边间距为空白区域为1/2
  • spaceBetween:将主轴方向上的空白区域均分,使children之间的空白区域相等,首尾child靠边没有间隙
  • spaceEvenly:将主轴方向上的空白区域均分,使得children之间的空白区域相等,包括首尾child
  • mainAxisSize max表示尽可能占多的控件,min会导致控件聚拢在一起
  • crossAxisAlignment 交叉轴方向,对child起作用
  • baseline:使children baseline对齐
  • center:children在交叉轴上居中展示
  • end:children在交叉轴上末尾展示
  • start:children在交叉轴上起点处展示
  • stretch:让children填满交叉轴方向
  • verticalDirection ,child的放置顺序
  • VerticalDirection.down,在Row中就是从左边到右边,Column代表从顶部到底部
  • VerticalDirection.up,相反
Row
示例代码

ListView(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text("我是Row的子控件 "),
Text(“MainAxisAlignment.start”)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("我是Row的子控件 "),
Text(“MainAxisAlignment.center”)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text("我是Row的子控件 “),
Text(“MainAxisAlignment.end”)
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: VerticalDirection.up,
children: [
Text(” Hello World “, style: TextStyle(fontSize: 30.0),),
Text(” I am Jack "),
],
],
)

代码运行效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前3个Row很简单,只是设置了主轴方向的对齐方式;第四个Row测试的是纵轴的对齐方式,由于两个子Text字体不一样,所以其高度也不同,我们指定了verticalDirection值为VerticalDirection.up,即从低向顶排列,而此时crossAxisAlignment值为CrossAxisAlignment.start表示底对齐。大家可以参考上面Row和Column的主侧轴的示意图,看看布局是不是正确的,还有很多种情况就不一一列举了。

Column
示例代码

ListView(children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(“我是Colum的子控件”),
Text(“CrossAxisAlignment.start”),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(“我是Colum的子控件”),
Text(“CrossAxisAlignment.center”),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(“我是Colum的子控件”),
Text(“CrossAxisAlignment.end”),
],
),
],)

代码运行效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Column和Row差不多,只是布局方向不一样而已,大家可以参考着看,这里就不再赘述了。

实际使用

由于篇幅有限,我就不详细讲解实际遇到的问题了,只说现象和解决办法:

  • 如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有对最外面的Row或Column会占用尽可能大的空间,里面Row或Column所占用的空间为实际大小,如果要让里面的Colum或Row占满外部Colum或Row,可以使用Expanded widget
  • 如果使用Column发现超范围,可用SingleChildScrollView包裹,scrollDirection属性设置滑动方向
  • 使用Column嵌套ListView/GridView的时候,会报异常信息【Viewports expand in the scrolling direction to fill their container…】,这种情况flutter已给出解决办法,将ListView/GridView的 shrinkWrap属性设为true
  • 有的时候修改Row/Column的verticalDirection会得到很好的效果,比如需要页面在底部需要几个按键,也可以用Stack来布局,但是相对麻烦,而且有时还需要知道控件的大小,没有verticalDirection方便

弹性布局

弹性布局是一种允许子widget按照一定比例来分配父容器空间的布局方式,如果你知道了它的主轴方向,那就可以用Row或Column了,一般情况下,可以用Flex的地方都可以用Row或者Column一起使用,通常配合Expanded Widget来使用,同样Expanded也不能脱离Flex单独创建。

Expanded

Expanded继承自Flexible,Flexible是一个控制Row、Column、Flex等子组件如何布局的组件,它可以按比例“扩伸”Row、Column和Flex子widget所占用的空间。

const Expanded({
int flex = 1,
@required Widget child,
})

flex为弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。

示例代码

Row(children: [
RaisedButton(
onPressed: () {
print(‘点击红色按钮事件’);
},
color: Colors.red,
child: Text(‘红色按钮’),
),
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () {
print(‘点击黄色按钮事件’);
},
color: Colors.yellow,
child: Text(‘黄色按钮’),
),
),
RaisedButton(
onPressed: () {
print(‘点击粉色按钮事件’);
},
color: Colors.green,
child: Text(‘绿色按钮’),
),
])

代码运行效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Flexible和 Expanded的区别
  • Flexible组件必须是Row、Column、Flex等组件的后裔,并且从Flexible到它封装的Row、Column、Flex的路径必须只包括StatelessWidgets或StatefulWidgets组件(不能是其他类型的组件,像RenderObjectWidgets)
  • Row、Column、Flex会被Expanded撑开,充满主轴可用空间,而Flexible不强制子组件填充可用空间,这是因为fit属性的值不同,该属性在Expanded中为FlexFit.tight,Flexible为FlexFit.loose,区别在于tight表示强制使子控件填充剩余可用空间,loose表示最多填满其在父控件所设置的比例,所以loose默认即为控件的大小

流式布局

流式布局(Liquid)的特点(也叫"Fluid") 是页面元素的宽度按照屏幕分辨率进行适配调整,但整体布局不变。栅栏系统(网格系统),用户标签等。在Flutter中主要有Wrap和Flow两种Widget实现。

Wrap

在介绍Row和Colum时,如果子widget超出屏幕范围,则会报溢出错误,在Flutter中通过Wrap和Flow来支持流式布局,溢出部分则会自动折行。

源码属性解读

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 children = const [],
})

上述有很多属性和Row的相同,其意义其实也是相同的,这里我就不一一介绍了,主要介绍下不同的属性:

  • spacing:主轴方向子widget的间距
  • runSpacing:纵轴方向的间距
  • runAlignment:纵轴方向的对齐方式
示例代码

Wrap(
spacing: 10.0,
direction: Axis.horizontal,
alignment: WrapAlignment.start,
children: [
_card(‘关注’),
_card(‘推荐’),
_card(‘新时代’),
_card(‘小视频’),
_card(‘党媒推荐’),
_card(‘中国新唱将’),
_card(‘历史’),
_card(‘视频’),
_card(‘游戏’),
_card(‘头条号’),
_card(‘数码’),
],
)

Widget _card(String title) {
return Card(child: Text(title),);
}
}

运行效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

小结
  • 使用Wrap可以很轻松的实现流式布局效果
  • Wrap支持设置流式布局是纵向显示或者是横向显示
  • 可以使用alignment属性来控制widgets的布局方式
Flow

我们一般很少会使用Flow,因为其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先要考虑的是Wrap是否满足需求。Flow主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。Flow有如下优点:

  • 性能好;Flow是一个对child尺寸以及位置调整非常高效的控件,Flow用转换矩阵(transformation matrices)在对child进行位置调整的时候进行了优化:在Flow定位过后,如果child的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild 进行重绘,而context.paintChild在重绘时使用了转换矩阵(transformation matrices),并没有实际调整Widget位置。
  • 灵活;由于我们需要自己实现FlowDelegate的paintChildren()方法,所以我们需要自己计算每一个widget的位置,因此,可以自定义布局策略。 缺点:
  • 使用复杂.
  • 不能自适应子widget大小,必须通过指定父容器大小或实现TestFlowDelegate的getSize返回固定大小。
示例代码

我们对六个色块进行自定义流式布局:

Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
children: [
new Container(width: 80.0, height:80.0, color: Colors.red,),
new Container(width: 80.0, height:80.0, color: Colors.green,),
new Container(width: 80.0, height:80.0, color: Colors.blue,),
new Container(width: 80.0, height:80.0, color: Colors.yellow,),
new Container(width: 80.0, height:80.0, color: Colors.brown,),
new Container(width: 80.0, height:80.0, color: Colors.purple,),
],
)

实现TestFlowDelegate:

class TestFlowDelegate extends FlowDelegate {
EdgeInsets margin = EdgeInsets.zero;
TestFlowDelegate({this.margin});
@override
void paintChildren(FlowPaintingContext context) {
var x = margin.left;
var y = margin.top;
//计算每一个子widget的位置
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;
//绘制子widget(有优化)
context.paintChild(i,
transform: new Matrix4.translationValues(
x, y, 0.0));
x += context.getChildSize(i).width + margin.left + margin.right;
}
}
}

getSize(BoxConstraints constraints){
//指定Flow的大小
return Size(double.infinity,200.0);
}

@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}

最后

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司19年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

[外链图片转存中…(img-II1CDuz0-1714965916846)]

【Android部分高级架构视频学习资源】

Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值