Flutter框架分析分析系列文章:node
《Flutter框架分析(一)-- 总览和Window》数组
《Flutter框架分析(三)-- Widget,Element和RenderObject》markdown
《Flutter框架分析(四)-- Flutter框架的运行》app
以前的文章给你们介绍了Flutter渲染流水线的动画(animate), 构建(build)阶段。本篇文章会结合Flutter源码给你们介绍一下渲染流水线接下来的布局(layout)阶段。函数
如同Android,iOS,h5等其余框架同样,页面在绘制以前框架须要肯定页面内各个元素的位置和大小(尺寸)。对于页面内的某个元素而言,若是其包含子元素,则只需在知道子元素的尺寸以后再由父元素肯定子元素在其内部的位置就完成了布局。因此只要肯定了子元素的尺寸和位置,布局就完成了。Flutter框架的布局采用的是盒子约束(Box constraints)模型。其布局流程以下图所示:布局
RenderObject
。从根节点开始,每一个父节点启动子节点的布局流程,在启动的时候会传入
Constraits
,也即“约束”。Flutter使用最多的是盒子约束(Box constraints)。盒子约束包含4个域:最大宽度(
maxWidth
)最小宽度(
minWidth
)最大高度(
maxHeight
)和最小高度(
minHeight
)。子节点布局完成之后会肯定本身的尺寸(
size
)。
size
包含两个域:宽度(
width
)和高度(
height
)。父节点在子节点布局完成之后须要的时候能够获取子节点的尺寸(
size
)总体的布局流程能够描述为一下一上,一下就是约束从上往下传递,一上是指尺寸从下往上传递。这样Flutter的布局流程只须要一趟遍历render tree便可完成。具体布局过程是如何运行的,咱们经过分析源码来进一步分析一下。
回顾《Flutter框架分析(四)-- Flutter框架的运行》咱们知道在vsync信号到来之后渲染流水线启动,在engine回调window
的onDrawFrame()
函数。这个函数会运行Flutter的“持久帧回调”(PERSISTENT FRAME CALLBACKS)。渲染流水线的构建(build),布局(layout)和绘制(paint)阶段都是在这个回调里,WidgetsBinding.drawFrame()。这个函数是在RendererBinding初始化的时候加入到“Persistent”回调的。
void drawFrame() {
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
...
}
}
复制代码
代码里的这一行buildOwner.buildScope(renderViewElement)
是渲染流水线的构建(build)阶段。这部分咱们在《Flutter框架分析(四)-- Flutter框架的运行》作了说明。而接下来的函数super.drawFrame()
会走到RendererBinding
中。
void drawFrame() {
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.
}
复制代码
里面的第一个调用pipelineOwner.flushLayout()
就是本篇文章要讲的布局阶段了。好了,咱们就从这里出发吧。先来看看PiplineOwner.flushLayout()
。
void flushLayout() {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
}
复制代码
这里会遍历dirtyNodes
数组。这个数组里放置的是须要从新作布局的RenderObject
。遍历以前会对dirtyNodes
数组按照其在render tree中的深度作个排序。这里的排序和咱们在构建(build)阶段遇到的对element tree的排序同样。排序之后会优先处理上层节点。由于布局的时候会递归处理子节点,这样若是先处理上层节点的话,就避免了后续重复布局下层节点。以后就会调用RenderObject._layoutWithoutResize()
来让节点本身作布局了。
void _layoutWithoutResize() {
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
...
}
_needsLayout = false;
markNeedsPaint();
}
复制代码
在RenderObject
中,函数performLayout()
须要其子类自行实现。由于有各类各样的布局,就须要子类个性化的实现本身的布局逻辑。在布局完成之后,会将自身的_needsLayout
标志置为false
。回头看一下上一个函数,在循环体里,只有_needsLayout
是true
的状况下才会调用_layoutWithoutResize()
。咱们知道在Flutter中布局,渲染都是由RenderObject
完成的。大部分页面元素使用的是盒子约束。RenderObject
有个子类RenderBox
就是处理这种布局方式的。而Flutter中大部分Widget
最终是由RenderBox
子类实现最终渲染的。源代码中的注释里有一句对RenderBox
的定义
A render object in a 2D Cartesian coordinate system.
翻译过来就是一个在二维笛卡尔坐标系中的render object。每一个盒子(box)都有个size
属性。包含高度和宽度。每一个盒子都有本身的坐标系,左上角为坐标为(0,0)。右下角坐标为(width, height)。
abstract class RenderBox extends RenderObject {
...
Size _size;
...
}
复制代码
咱们在写Flutter app的时候设定组件大小尺寸的时候都是在建立Widget
的时候把尺寸或者相似居中等这样的配置传进去。例如如下这个Widget
咱们规定了它的大小是100x100;
Container(width: 100, height: 100);
复制代码
由于布局是在RenderObject
里完成的,这里更具体的说应该是RenderBox
。那么这个100x100的尺寸是如何传递到RenderBox
的呢?RenderBox
又是如何作布局的呢? Container
是个StatelessWidget
。它自己不会对应任何RenderObject
。根据构造时传入的参数,Container
最终会返回由Align
,Padding
,ConstrainedBox
等组合而成的Widget
:
Container({
Key key,
this.alignment,
this.padding,
Color color,
Decoration decoration,
this.foregroundDecoration,
double width,
double height,
BoxConstraints constraints,
this.margin,
this.transform,
this.child,
}) : decoration = decoration ?? (color != null ? BoxDecoration(color: color) : null),
constraints =
(width != null || height != null)
? constraints?.tighten(width: width, height: height)
?? BoxConstraints.tightFor(width: width, height: height)
: constraints,
super(key: key);
final BoxConstraints constraints;
@override
Widget build(BuildContext context) {
Widget current = child;
if (child == null && (constraints == null || !constraints.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
if (alignment != null)
current = Align(alignment: alignment, child: current);
final EdgeInsetsGeometry effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (decoration != null)
current = DecoratedBox(decoration: decoration, child: current);
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null)
current = ConstrainedBox(constraints: constraints, child: current);
if (margin != null)
current = Padding(padding: margin, child: current);
if (transform != null)
current = Transform(transform: transform, child: current);
return current;
}
复制代码
在本例中返回的是一个ConstrainedBox
。
class ConstrainedBox extends SingleChildRenderObjectWidget {
ConstrainedBox({
Key key,
@required this.constraints,
Widget child,
}) : assert(constraints != null),
assert(constraints.debugAssertIsValid()),
super(key: key, child: child);
/// The additional constraints to impose on the child.
final BoxConstraints constraints;
@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(additionalConstraints: constraints);
}
@override
void updateRenderObject(BuildContext context, RenderConstrainedBox renderObject) {
renderObject.additionalConstraints = constraints;
}
}
复制代码
而这个Widget
对应的会建立RenderConstrainedBox
。那么具体的布局工做就是由它来完成的,而且从上述代码可知,那个100x100的尺寸就在constraints
里面了。
class RenderConstrainedBox extends RenderProxyBox {
RenderConstrainedBox({
RenderBox child,
@required BoxConstraints additionalConstraints,
}) :
_additionalConstraints = additionalConstraints,
super(child);
BoxConstraints _additionalConstraints;
@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
继承自RenderProxyBox
。而RenderProxyBox
则又继承自RenderBox
。
在这里咱们看到了performLayout()
的实现。当有孩子节点的时候,这里会调用child.layout()
请求孩子节点作布局。调用时要传入对孩子节点的约束constraints
。这里会把100x100的约束传入。在孩子节点布局完成之后把本身的尺寸设置为孩子节点的尺寸。没有孩子节点的时候就把约束转换为尺寸设置给本身。
咱们看一下child.layout()
。这个函数在RenderObject
类中:
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
...
}
}
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
...
}
_needsLayout = false;
markNeedsPaint();
}
复制代码
这个函数比较长一些,也比较关键。首先作的事情是肯定relayoutBoundary
。这里面有几个条件:
parentUsesSize
:父组件是否须要子组件的尺寸,这是调用时候的入参,默认为false
。sizedByParent
:这是个RenderObject
的属性,表示当前RenderObject
的布局是否只受父RenderObject
给与的约束影响。默认为false
。子类若是须要的话能够返回true
。好比RenderErrorBox
。当咱们的Flutter app出错的话,屏幕上显示出来的红底黄字的界面就是由它来渲染的。constraints.isTight
:表明约束是不是严格约束。也就是说是否只容许一个尺寸。RenderObject
。 在以上条件任一个知足时,relayoutBoundary
就是本身,不然取父节点的relayoutBoundary
。接下来是另外一个判断,若是当前节点不须要作从新布局,约束也没有变化,relayoutBoundary
也没有变化就直接返回了。也就是说从这个节点开始,包括其下的子节点都不须要作从新布局了。这样就会有性能上的提高。
而后是另外一个判断,若是sizedByParent
为true
,会调用performResize()
。这个函数会仅仅根据约束来计算当前RenderObject
的尺寸。当这个函数被调用之后,一般接下来的performLayout()
函数里不能再更改尺寸了。
performLayout()
是大部分节点作布局的地方了。不一样的RenderObject
会有不一样的实现。
最后标记当前节点须要被重绘。布局过程就是这样递归进行的。从上往下一层层的叠加不一样的约束,子节点根据约束来计算本身的尺寸,须要的话,父节点会在子节点布局完成之后拿到子节点的尺寸来作进一步处理。也就是咱们开头说的一下一上。
调用layout()
的时候咱们须要传入约束,那么咱们就来看一下这个约束是怎么回事:
abstract class Constraints {
bool get isTight;
bool get isNormalized;
}
复制代码
这是个抽象类,仅有两个getter
。isTight
就是咱们以前说的严格约束。由于Flutter中主要是盒子约束。因此咱们来看一下Constraints
的子类:BoxConstraints
class BoxConstraints extends Constraints {
const BoxConstraints({
this.minWidth = 0.0,
this.maxWidth = double.infinity,
this.minHeight = 0.0,
this.maxHeight = double.infinity,
});
final double minWidth;
final double maxWidth;
final double minHeight;
final double maxHeight;
...
}
复制代码
盒子约束有4个属性,最大宽度,最小宽度,最大高度和最小高度。这4个属性的不一样组合构成了不一样的约束。
当在某一个轴方向上最大约束和最小约束是相同的,那么这个轴方向被认为是严格约束(tightly constrained)的。
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
const BoxConstraints.tightFor({
double width,
double height,
}) : minWidth = width != null ? width : 0.0,
maxWidth = width != null ? width : double.infinity,
minHeight = height != null ? height : 0.0,
maxHeight = height != null ? height : double.infinity;
BoxConstraints tighten({ double width, double height }) {
return BoxConstraints(minWidth: width == null ? minWidth : width.clamp(minWidth, maxWidth),
maxWidth: width == null ? maxWidth : width.clamp(minWidth, maxWidth),
minHeight: height == null ? minHeight : height.clamp(minHeight, maxHeight),
maxHeight: height == null ? maxHeight : height.clamp(minHeight, maxHeight));
}
复制代码
当在某一个轴方向上最小约束是0.0,那么这个轴方向被认为是宽松约束(loose)的。
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
BoxConstraints loosen() {
assert(debugAssertIsValid());
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
复制代码
当某一轴方向上的最大约束的值小于double.infinity
时,这个轴方向的约束是有限制的。
bool get hasBoundedWidth => maxWidth < double.infinity;
bool get hasBoundedHeight => maxHeight < double.infinity;
复制代码
当某一轴方向上的最大约束的值等于double.infinity
时,这个轴方向的约束是无限制的。若是最大最小约束都是double.infinity
,这个轴方向的约束是扩展的(exbanding)。
const BoxConstraints.expand({
double width,
double height,
}) : minWidth = width != null ? width : double.infinity,
maxWidth = width != null ? width : double.infinity,
minHeight = height != null ? height : double.infinity,
maxHeight = height != null ? height : double.infinity;
复制代码
最后,在布局的时候节点须要把约束转换为尺寸。这里获得的尺寸被认为是知足约束的。
Size constrain(Size size) {
Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
return result;
}
double constrainWidth([ double width = double.infinity ]) {
return width.clamp(minWidth, maxWidth);
}
double constrainHeight([ double height = double.infinity ]) {
return height.clamp(minHeight, maxHeight);
}
复制代码
咱们知道render tree的根节点是RenderView
。在RendererBinding
建立RenderView
的时候会传入一个ViewConfiguration
类型的配置参数:
void initRenderView() {
assert(renderView == null);
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.scheduleInitialFrame();
}
复制代码
ViewConfiguration
定义以下,包含一个尺寸属性和一个设备像素比例属性:
@immutable
class ViewConfiguration {
const ViewConfiguration({
this.size = Size.zero,
this.devicePixelRatio = 1.0,
});
final Size size;
final double devicePixelRatio;
}
复制代码
ViewConfiguration
实例由函数createViewConfiguration()
建立:
ViewConfiguration createViewConfiguration() {
final double devicePixelRatio = window.devicePixelRatio;
return ViewConfiguration(
size: window.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio,
);
}
复制代码
可见,尺寸取的是窗口的物理像素大小再除以设备像素比例。在Nexus5上,全屏窗口的物理像素大小(window.physicalSize
)是1080x1776。设备像素比例(window.devicePixelRatio
)是3.0。最终ViewConfiguration
的size
属性为360x592。
那么咱们来看一下RenderView
如何作布局:
@override
void performLayout() {
_size = configuration.size;
if (child != null)
child.layout(BoxConstraints.tight(_size));
}
复制代码
根节点根据配置的尺寸生成了一个严格的盒子约束,以Nexus5为例的话,这个约束就是最大宽度和最小宽度都是360,最大高度和最小高度都是592。在调用子节点的layout()
的时候传入这个严格约束。
假如咱们想在屏幕居中位置显示一个100x100的矩形,代码以下:
runApp(Center(child: Container(width: 100, height: 100, color: Color(0xFFFF9000),)));
复制代码
运行之后则render tree结构以下:
RenderView
的子节点是个RenderPositionedBox
。其布局函数以下:
@override
void performLayout() {
if (child != null) {
child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.infinity));
alignChild();
}
}
复制代码
这里的constraints
来自根节点RenderView
。咱们以前分析过,这是一个360x592的严格约束。在调用孩子节点的layout()
时候会给孩子节点一个新的约束,这个约束是把本身的严格约束宽松之后的新约束,也就是说,给子节点的约束是[0-360]x[0-592]。而且设置了parentUsesSize
为true
。
接下来就是子节点RenderConstrainedBox
来布局了:
@override
void performLayout() {
if (child != null) {
child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
复制代码
这里又会调用子节点RenderDecoratedBox
的布局函数,给子节点的约束是啥样的呢? _additionalConstraints
来自咱们给咱们在Container
中设置的100x100大小。从前述分析可知,这是个严格约束。而父节点给过来的是[0-360]x[0-592]。经过调用enforce()
函数生成新的约束:
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),
);
}
复制代码
从上述代码可见,新的约束就是100x100的严格约束了。最后咱们就来到了叶子节点(RenderDecoratedBox
)的布局了:
@override
void performLayout() {
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = child.size;
} else {
performResize();
}
}
复制代码
由于是叶子节点,它没有孩子,因此走的是else
分支,调用了performResize()
:
@override
void performResize() {
size = constraints.smallest;
}
复制代码
没有孩子的时候默认布局就是使本身在当前约束下尽量的小。因此这里获得的尺寸就是100x100;
至此布局流程的“一下”这个过程就完成了。可见,这个过程就是父节点根据本身的配置生成给子节点的约束,而后让子节点根据父节点的约束去作布局。
“一下”作完了,那么就该“一上”了。 回到叶子节点的父节点RenderConstrainedBox
:
child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child.size;
复制代码
没干啥,把孩子的尺寸设成本身的尺寸,孩子多大我就多大。再往上,就到了RenderPositionedBox
:
child.layout(constraints.loosen(), parentUsesSize: true);
size = constraints.constrain(Size(shrinkWrapWidth ? child.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child.size.height * (_heightFactor ?? 1.0) : double.infinity));
alignChild();
复制代码
这里shrinkWrapWidth
和shrinkWrapHeight
都是false
。而约束是360x592的严格约束,因此最后获得的尺寸就是360x592了。而孩子节点是100x100,那就须要知道把孩子节点放在本身内部的什么位置了,因此要调用alignChild()
void alignChild() {
_resolve();
final BoxParentData childParentData = child.parentData;
childParentData.offset = _resolvedAlignment.alongOffset(size - child.size);
}
复制代码
孩子节点在父节点内部的对齐方式由Alignment
决定。
class Alignment extends AlignmentGeometry {
const Alignment(this.x, this.y)
final double x;
final double y;
@override
double get _x => x;
@override
double get _start => 0.0;
@override
double get _y => y;
/// The top left corner.
static const Alignment topLeft = Alignment(-1.0, -1.0);
/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0, -1.0);
/// The top right corner.
static const Alignment topRight = Alignment(1.0, -1.0);
/// The center point along the left edge.
static const Alignment centerLeft = Alignment(-1.0, 0.0);
/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0, 0.0);
/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0, 0.0);
/// The bottom left corner.
static const Alignment bottomLeft = Alignment(-1.0, 1.0);
/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0, 1.0);
/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0, 1.0);
复制代码
其内部包含两个浮点型的系数。经过这两个系数的组合就能够定义出咱们通用的一些对齐方式,好比左上角是Alignment(-1.0, -1.0)
。顶部居中就是Alignment(0.0, -1.0)
。右上角就是Alignment(1.0, -1.0)
。咱们用到的垂直水平都居中就是Alignment(0.0, 0.0)
。那么怎么从Alignment
来计算偏移量呢?就是经过咱们在上面见到的 Alignment.alongOffset(size - child.size)
调用了。
Offset alongOffset(Offset other) {
final double centerX = other.dx / 2.0;
final double centerY = other.dy / 2.0;
return Offset(centerX + x * centerX, centerY + y * centerY);
}
复制代码
入参就是父节点的尺寸减去子节点的尺寸,也就是父节点空余的空间。分别取空余长宽而后除以2获得中值。而后每一个中值在加上Alignment
的系数乘以这个中值就获得了偏移量。是否是很巧妙?咱们的例子是垂直水平都居中,x
和y
都是0。因此可得偏移量就是[130,246]。
回到alignChild()
,在取得偏移量以后,父节点会经过设置childParentData.offset
把这个偏移量保存在孩子节点那里。这个偏移量在后续的绘制流程中会被用到。
最后就回到了根节点RenderView
。至此布局流程的“一上”也完成了。可见这个后半段流程父节点有可能根据子节点的尺寸来决定本身的尺寸,同时也有可能要根据子节点的尺寸和本身的尺寸来决定子节点在其内部的位置。
本篇文章介绍了Flutter渲染流水线的布局(layout)阶段,布局(layout)阶段主要就是要掌握住“一下一上”过程,一下就是约束层层向下传递,一上就是尺寸层层向上传递。本篇并无过多介绍各类布局的细节,你们只要掌握了布局的流程,具体哪一种布局是如何实现的只须要查阅对应RenderObject
的源码就能够了。