flutter的widget是如何计算尺寸和位置的,通过一个非常简单的代码结合源码来分析
1、widget树生成element树,element树生成RenderObject树,实际参与布局的就是RenderObject树,后续的源码分析也是针对RenderObject
2、flutter的布局约束,都是采用BoxConstraints来实现,一共有四个参数
// 最小宽度
final double minWidth;
// 最大宽度
final double maxWidth;
// 最小高度
final double minHeight;
// 最大高度
final double maxHeight;
bool get isTight => hasTightWidth && hasTightHeight;
当minWidth==maxWidth并且minHeight==maxHeight,则isTight
是true,代表是严格约束,宽高的值就是确定的了
先看下代码,非常简单,就是屏幕中间展示一个黄色的色块,色块的长宽分别是100
runApp(Center(
child: Container(
width: 100,
height: 100,
color: const Color(0xFFFF9000),
)));
运行后的效果图如下
可以在dev tool看到最终生成的widget树如下
最外层的root是系统生成的,上面有说过,所有的布局都是由renderObject来处理,最外层的root的renderObject就是RenderView
,布局的调用逻辑,就是由外层的RenderObject调用内层,一级级调用下去,最外层就是root的performLayout
方法
void performLayout() {
assert(_rootTransform != null);
_size = configuration.size;
assert(_size.isFinite);
if (child != null)
child!.layout(BoxConstraints.tight(_size));
}
上面的child就是Center这个widget生成的对应的renderObject,至于_size
是由configuration.size
决定的,configuration
的生成代码如下
ViewConfiguration createViewConfiguration() {
final double devicePixelRatio = window.devicePixelRatio;
final Size size = _surfaceSize ?? window.physicalSize / devicePixelRatio;
return ViewConfiguration(
size: size,
devicePixelRatio: devicePixelRatio,
);
}
configuration
是系统的屏幕分辨率除以像素比,比如我目前的手机一加10T来说,屏幕分辨率是1080*2352,屏幕像素比是3.0,最终的尺寸就是Size(360.0, 784.0)
继续看下BoxConstraints.tight
方法,就是返回严格约束,说明root布局约束的长宽就是屏幕的尺寸
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
接下来,由最外层的root去加载我们的第一个widget:Center
先看下Center组件的源码
class Center extends Align {
/// Creates a widget that centers its child.
const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
center继承了Align组件,什么属性都没改,非常简单,因为Align组件默认align属性是:Alignment.center
,所以原始的代码,我们把Center换成Align效果也是一样的
runApp(Align(
child: Container(
width: 100,
height: 100,
color: const Color(0xFFFF9000),
)));
Align组件对应的renderObject是RenderPositionedBox
,看下它布局的代码
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
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();
} else {
size = constraints.constrain(Size(
shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity,
));
}
}
由于child不为空,shrinkWrapWidth跟shrinkWrapHeight都是false,上面的代码,我做个精简,如下
void performLayout() {
// 这里的constraints就是手机的屏幕尺寸
final BoxConstraints constraints = this.constraints;
// 让child先布局
child!.layout(constraints.loosen(), parentUsesSize: true);
// 最终的size就还是手机屏幕尺寸
size = constraints.constrain(Size(double.infinity,
double.infinity,
));
// 计算子布局的offset
alignChild();
}
先让child去参与布局计算尺寸,最终child计算的尺寸是Size(100.0, 100.0)
(稍后分析),Center组件最终的size就是手机屏幕的尺寸,也就是Size(360.0, 784.0)
,那是如何实现child居中的效果的呢?
这个是因为每个RenderObject有一个ParentData.offset
参数,用于告诉父renderObject在绘制子的时候,子布局的偏移量
/// The offset at which to paint the child in the parent's coordinate system.
Offset offset = Offset.zero;
而这个offset的设值,就是在alignChild
方法上
void alignChild() {
_resolve();
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
根据居中的规格来计算偏移量,就是(父布局的尺寸-子布局计算的尺寸)/2,刚好得出的就是居中的效果
// 居中效果的x,y值都是0
static const Alignment center = Alignment(0.0, 0.0);
// 计算的偏移量刚好就是居中效果
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);
}
最终得出的偏移值是Offset(130.0, 342.0)
,采用这个偏移量去绘制出来的时候,就是刚好居中了
Center组件嵌套的child,是一个contaienr组件,container是statelessWidget,本身没有对应的RenderObject,具体是根据不同的情况,会生成不同的RenderObject,就我们这个例子而言,是生成了RenderConstrainedBox
,最终调用的绘制方法如下
void performLayout() {
final BoxConstraints constraints = this.constraints;
if (child != null) {
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
这里的constraints的值是BoxConstraints(0.0<=w<=360.0, 0.0<=h<=784.0)
,代表父类对子类的约束,就是你的尺寸最大不要超过我,最小的我不管,随便你多小,_additionalConstraints是额外的约束,是我们代码上写的约束信息,值是:BoxConstraints(w=100.0, h=100.0)
,代表是写死的宽高都是100;
_additionalConstraints.enforce(constraints)的结果也就是约束的100*100,这个就是给child的约束尺寸信息
这里要重点看下child!.layout
方法,方法源码很长,我就截取关键的部分做下解析
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// 看后面分析
RenderObject? relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
// 符合条件,直接返回,避免重复计算布局,优化性能
return;
}
_constraints = constraints;
RenderObject? debugPreviousActiveLayout;
try {
performLayout();
} catch (e, stack) {
}
// 已经布局过了,把layout标记设置为false
_needsLayout = false;
// 因为重新布局了,所以标记下需要绘制,触发重新绘制
markNeedsPaint();
}
layout方法有个parentUsesSize
参数,代表父类是否需要基于child的大小来计算其自身的大小,默认是false,这里传的是true,代表父类的布局跟child的大小有关,这样当child被设置为需要重新layout的时候,也会触发父布局的重新layout
还有个relayoutBoundary参数,是用于判断当前布局是基于哪个RenderObject来计算的,在下面四种场景 1、其父布局不需要它的尺寸计算自身的尺寸 2、当前子布局尺寸是完全由父布局约束决定,子布局自己内部节点等都不影响最终的尺寸 3、约束是严格约束 4、父布局不是一个RenderObject 符合上面四种的一种,就代表relayoutBoundary就是它自己,其他情况下,relayoutBoundary就是父布局的relayoutBoundary
在接下来,会判断relayoutBoundary没变,_needsLayout是false,并且约束也没变,就不会重新去计算布局了,提升性能
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
继续看下最内部的child,实际的类型是_RenderColoredBox,就是用于绘制颜色的方块,继续看下它的layout方法
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
constrains的值就是BoxConstraints(w=100.0, h=100.0)
,由于child已经是最里面布局了,它没有child了,代码走到else里
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.smallest;
}
返回当前约束下最小的尺寸,由于尺寸是tight类型,最小布局也是100,所以最终的布局尺寸就是100*100,也就是中间色块的大小
最外面的root是由系统生成的,尺寸就是屏幕的大小,然后由root逐渐往里面遍历,只遍历一次,就计算出各组件的尺寸了,所有子组件的尺寸都是由父组件的约束条件,加上子组件对自身的规格计算出来的