总结了30个例子之后,我悟到了Flutter的布局原理
学习最忌盲目,无计划,零碎的知识点无法串成系统。学到哪,忘到哪,面试想不起来。这里我整理了Flutter面试中最常问以及Flutter framework中最核心的几块知识,大概二十篇左右文章分析,欢迎关注,共同进步。
导语
UI原理部分:
1、为什么不建议大家使用setState()。
2、面试官问我State的生命周期,该怎么回答
3、Flutter的布局约束原理
4、实战Flutter绘制过程
读完本文你将收获:Flutter各种控件是如何实现布局的
引言
一切的开始来自于这个神奇的网站(深入理解Flutter布局约束),其中提供了30个让奇奇怪怪的布局例子让我大涨见识(30个相当不讲武德!!),有这样的
这样的
还有这样的
不得不说确实覆盖了很多场景!可是对于我这种记性不好的懒鬼来说,看完30个例子真的是太!费!劲!了!而且看完就忘!!实际中大概率不会出现一模一样的情况。所以我就在寻思,这背后究竟是啥原理可以我下次不用反复复习这30个例子呢?这就引出了今天的主题:Flutter的布局原理
正片开始:先宏观看看Flutter组件的分类
在原生上我们知道一个View控件的渲染过程大致分为onMeasure()[知道有多大],onLayout()[知道该放那],onDraw()[知道长啥样]三个过程。但Flutter的UI体系思路和这个不太一样,首先在Flutter的组件体系中,并非所有的Widget都会渲染到最后的页面上,整个Widget大概可以分为三类组合类、代理类、绘制类 -[这点面试必问!!]-
平时我们使用到最多的StatelessWidget和StatefulWidget其实只是组合类的控件,实际上他并不负责绘制,所有我们在屏幕上看到的UI最终几乎都会通过RenderObjectWidget实现。而RenderObjectWidget中有个createRenderObject()方法生成RenderObject对象,RenderObject实际负责实际的layout()和paint()。例如我们最常使用的Container组件其实只是一个组合类的控件,在其中封装了多个负责绘制的原子组件。想详细了解RenderObjectWidget可以看看深入研究Flutter布局原理写得非常好
开胃小菜:RenderObject的的绘制过程
那RenderObject是如何完成渲染的呢,在原来我一直在错误的使用 setState()?中分析过,Flutter的渲染流程关键在于drawFrame()方法中
void drawFrame() {
//在这之前已经完成了build()
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
整个过程和原生分为三个阶段build(),layout(),paint()。build()方法由组合类和代理类Widget实现,layout()和paint()由RenderObject实现。这里设计思路和原生不太一样,在原生中layout()方法一般由ViewGroup实现,他需要规定child控件的位置。这样他的子节点就只用关心绘制即可。
而Flutter中的layout()更接近理解为Measure(!!理解这点非常重要,不能用原生的思路去学习),它的职能主要是计算控件自身的尺寸和位置偏移。这里的计算是一个从最顶级的节点开始传递约束,从下开始返回测量结果的过程
测量结果我们很好理解,就是一个控件实际的宽高。而约束是个啥玩意儿??
硬菜来了: What's Constraints
约束Constraints 在Flutter中是一种布局协议,Flutter中有两大布局协议BoxConstraints和SliverConstraints。对于非滑动的控件例如Padding,Flex等一般都使用BoxConstraints盒约束。
BoxConstraints({
this.minWidth,
this.maxWidth,
this.minHeight,
this.maxHeight,
});
看起来非常好理解,在盒约束中,只会限制子控件的最大最小宽高。经过我搜刮了网上几乎所有的布局原理文章之后,对于这个约束这个约束可以这样总结。 首先这个约束可以根据最大最小值分为两大类
- 1、 tight(紧约束):当max和min值相等时,这时传递给子类的是一个确定的宽高值。
const BoxConstraints.expand({
double width,
double height,
}) : minWidth = width ?? double.infinity,
maxWidth = width ?? double.infinity,
minHeight = height ?? double.infinity,
maxHeight = height ?? double.infinity;
这个约束的使用的地方主要有两个
一个在Container中,当Container的 child==null&&||(constraints == null || !constraints.isTight))时。 另一个ModalBarrier,这个组件我们不太熟悉,但查看调用发现被嵌套在了Route中,所以每次我们push一个新Route的时候,默认新的页面就是撑满屏幕的模式。
- 2、loose(松约束):当max和min不相等的时候,这种时候对子类的约束是一个范围,称为松约束。
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
在我们最常使用的Scaffold组件中就采用了这种布局,所以Scaffold对于子布局传递的是一个松的约束。
酣畅过瘾: 举几个栗子
了解了上面的基础概念之后,我们先来看看如何对一些场景进行分析。当我们想知道一个控件的布局过程是怎样的,可以参考:
在你的代码中找到一个 Column 并跟进到它的源代码。为此,请在 (Android Studio/IntelliJ) 中使用 command+B(macOS)或 control+B(Windows/Linux)。你将跳到 basic.dart 文件中。由于 Column 扩展了 Flex,请导航至 Flex 源代码(也位于 basic.dart 中)。 向下滚动直到找到一个名为 createRenderObject() 的方法。如你所见,此方法返回一个 RenderFlex。它是 Column 的渲染对象,现在导航到 flex.dart 文件中的 RenderFlex 的源代码。 向下滚动,直到找到 performLayout() 方法,由该方法执行列布局。
根据这个方法,我们试着分析几个有意思的栗子。
案例1(来自样例一)
如图,如果我们直接返回一个红色Container,这个时候他会撑满整个屏幕。首先Container是一个组合类的Widget,并不负责渲染。查看他的build方法,在这种情况下返回了三层RenderObject RenderDecoratedBox,RenderLimitedBox,RenderConstrainedBox
image这三个类都继承自RenderProxyBox,这个类混入了RenderProxyBoxMixin,布局方法就在里面:
@override
void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
其实从这个类的名称我们可知一二,这是个代理类的渲染对象,如果他有子节点的的时候他会把自己父节点的约束constraints传递给节点,然后使用子节点的尺寸作为自己的。 那么最外层的RenderDecoratedBox的约束是什么呢。前面其实也提到了在紧约束中,BoxConstraints.expand被用在了Route上,所以每个页面默认是撑满屏幕的。这个约束就一直向下传递
image但在最下面的RenderConstrainedBox重写了performLayout方法
@override
void performLayout() {
if (child != null) {
child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
其实和上面差不多,不过对RenderConstrainedBox我们可以添加约束信息_additionalConstraints属性,查看Container的build方法可知这个约束在这种情况下为BoxConstraints.expand ,在layout的时候也会被考虑。由于RenderConstrainedBox下没有child了所以走
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
///返回新的框约束,它尊重给定的约束,同时与原始约束尽可能接近
BoxConstraints enforce(BoxConstraints constraints) {
return BoxConstraints(
minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
);
}
关键在于enforce这个方法,这里他接受到的参数是来自页面中的紧约束(max=min=屏幕宽度),这样无论你为自身添加的约束_additionalConstraints是多少,他都会返回一个紧约束(max=min=屏幕宽度),而这个约束被再次向下传递
image最终这个Container渲染的时候,撑满了整个屏幕!
案例2(来自样例9)
这个例子案例1外层增加了一个约束,并且内部也为自己设置了高度。你可能会猜想 Container 的尺寸会在 70 到 150 像素之间,但并不是这样,Container仍然撑满了整个屏幕。我们还是来看Render树的结构
整个树构造变成了这样,我们一步步来看。 首先最外层的ConstrainedBox接收到来自页面的紧约束(max=min=屏幕宽度),计算采用上面的计算方式还是得到了一个同样的约束,之后这个约束向下传递,就变成和案例一一样的情况
总结
1、Widget大概可以分为三类组合类、代理类、绘制类
2、所有我们在屏幕上看到的UI最终几乎都会通过RenderObjectWidget实现。而RenderObjectWidget中有个createRenderObject()方法生成RenderObject对象,RenderObject实际负责实际的layout()和paint()。
3、Container组件其实只是一个组合类的控件,在其中封装了多个负责绘制的原子组件。
4、layout() 职能主要是计算控件自身的尺寸和位置偏移
5、整个布局过程就是向下约束 向上传值的过程
6、盒约束中有两种:
- tight(紧约束):当max和min值相等时,这时传递给子类的是一个确定的宽高值。
- loose(松约束):当max和min不相等的时候,这种时候对子类的约束是一个范围,称为松约束
一句话总结:
最后
在这里我也分享一份由几位大佬一起收录整理的 Flutter进阶资料以及Android学习PDF+架构视频+面试文档+源码笔记 ,并且还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料……
这些都是我闲暇时还会反复翻阅的精品资料。可以有效的帮助大家掌握知识、理解原理。当然你也可以拿去查漏补缺,提升自身的竞争力。
如果你有需要的话,可以前往 GitHub 自行查阅。
相信一定可以帮助到大家!
网友评论