总结了 30 个例子之后,我悟到了 Flutter 的布局原理,android 移动开发基础答案
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?在 Fl
utter 中是一种_布局协议_,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
这三个类都继承自 RenderProxyBox,这个类混入了 RenderProxyBoxMixin,布局方法就在里面:
@override
void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
其实从这个类的名称我们可知一二,这是个代理类的渲染对象,如果他有子节点的的时候他会把自己父节点的约束 constraints 传递给节点,然后使用子节点的尺寸作为自己的。 那么最外层的 RenderDecoratedBox 的约束是什么呢。前面其实也提到了在紧约束中,_BoxConstraints.expand_被用在了 Route 上,所以每个页面默认是撑满屏幕的。这个约束就一直向下传递
评论