问题描述
用一个Container包含一个Image发现设置的宽高40不生效
less
Container(
width: 300,
height: 400,
color: Colors.blue,
child: Image.asset(
"assets/img/ic_file_pdf.png",
fit: BoxFit.cover,
width: 40,
height: 40,
),
),
展示效果

布局的主要函数流程

布局绘制的主要逻辑都是在RenderObject里面的,RenderObject是所有渲染对象的基类,定义了基本的布局流程和事件分发流程,主要函数就是layout
scss
void layout(Constraints constraints, { bool parentUsesSize = false }) {
.....
_constraints = constraints; //将父组件传递下来的约束赋值给自己的_constraints
if (sizedByParent) {
try {
performResize();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performResize', e, stack);
}
}
RenderObject? debugPreviousActiveLayout;
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_reportException('performLayout', e, stack);
}
}
这个函数关键调用了performResize和performLayout方法,前面的方法用于计算大小,计算出来的结果放在size属性里面,后面的方法用于布局,根据不同的布局算法,把组件放置到不同的地方,performResize方法被调用有个前提就是sizedByParent=true,意思就是大小只和父组件有关,和子组件没关系,具体含义先不去纠结。这两个方法都是抽象方法,需要子类实现
然后RenderBox在RenderObject上封装了一层,基本所有的盒模型类都是继承这个类,RenderBox封装出一个computeDryLayout这个方法,然后在performResize里面调用这个方法,所以如果继承RenderBox,就要实现computeDryLayout,用于计算组件的大小。
我们在实现performLayout的时候会调用child.layout方法,然后child又会调performLayout实现了遍历。
如何确定大小的
约束
flutter里面用约束来确定大小,约束对应Constraints类,一般用其子类BoxConstraints,有四个参数界定约束的大小
ini
const BoxConstraints({
this.minWidth = 0.0,
this.maxWidth = double.infinity,
this.minHeight = 0.0,
this.maxHeight = double.infinity,
});
这四个参数定义了约束的性质,意思是最小是多少,最大是多少。在宽高两个方向分别约束。
我们可以想一下,我们在写布局的时候可以分为三种情况:
- 第一种是根据子类内容来决定自己的宽度,子组件撑起多大自己就多大,但是不能超过父组件允许的最大范围
- 第二种是不管子组件多大父组件能让我占多大,我就占多大;
- 第三种是我有自己的确定宽高,不受其他组件影响,但是确定的宽高不能超过父组件范围
综合前面几种情况就比较容易理解约束里面为什么有maxHeight和maxWidth,但是minWidth和minHeight同时规定了最小应该为多少,我一直比较有疑问为什么flutter里面需要约束最小为多少,印象里面其他UI框架里面好像没有约束最小为多少,既然flutter里面有这个就按照他的定义去理解。
约束一般分为两种类型
- 严格约束
本质上是最小和最大是一样的,意思就是只能这么大,不能是其他大小了,这种情况会有一些坑,有点反直觉
arduino
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
- 宽松约束
只规定了最大是多少,最小没有规定,默认是0,比较符合直觉
scss
BoxConstraints loosen() {
assert(debugAssertIsValid());
return BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
);
}
约束传递
在上面的布局流程中,父组件会在传递约束给子组件,子组件根据自身情况然后在父组件传递过来的约束下共同决定子组件有多大,传递的约束是在layout函数constraints参数里面
arduino
void layout(Constraints constraints, { bool parentUsesSize = false })
在上面的流程里面已经知道了在父组件的performLayout会调用child.layout的,所以看一下父组件是如何传入这个参数的
以Container为例,我们设置宽高为100,其对应的渲染widget是ConstrainedBox,渲染对象RenderConstrainedBox,找到其performLayout方法
arduino
void performLayout() {
final BoxConstraints constraints = this.constraints; //这里的constraints就是父组件传给自己的constraints
if (child != null) {
//_additionalConstraints就是自己在设置Container时设置的宽高,并且也是个强制约束
//enforce的含义是_additionalConstraints必须约束在constraints里面
child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
size = child!.size;
} else {
size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
}
}
根据上面的代码注释就能知道,Container传递给子组件的约束是根据自身的约束条件和Container父组件传递过来的约束有关,如果父组件也是严格约束,那么传递给子组件的约束其实是Container父组件的严格约束
确定大小
子组件拿到这个约束后需要结合自身情况综合考量,这个步骤一般在渲染类的computeDryLayout里面进行
以Text组件为例,其对应的渲染类是RenderParagraph
less
@override
@protected
Size computeDryLayout(covariant BoxConstraints constraints) {
if (!_canComputeIntrinsics()) {
assert(debugCannotComputeDryLayout(
reason: 'Dry layout not available for alignments that require baseline.',
));
return Size.zero;
}
final Size size = (_textIntrinsics
..setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild))
..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth)))
.size;
return constraints.constrain(size);
}
size其实就是Text文案计算出来的范围,然后这个范围必须在父组件约束的范围内,看一下constraints.constrain定义
arduino
Size constrain(Size size) {
Size result = Size(constrainWidth(size.width), constrainHeight(size.height));
assert(() {
result = _debugPropagateDebugSize(size, result);
return true;
}());
return result;
}
double constrainWidth([ double width = double.infinity ]) {
assert(debugAssertIsValid());
return clampDouble(width, minWidth, maxWidth);
}
//clampDouble函数其实就是夹断函数
double clampDouble(double x, double min, double max) {
assert(min <= max && !max.isNaN && !min.isNaN);
if (x < min) {
return min;
}
if (x > max) {
return max;
}
if (x.isNaN) {
return max;
}
return x;
}
所以如果父组件传递的是严格约束,那么Text的最终大小就是严格约束的大小,自己计算的文字大小并不是最终的大小。
问题解决
用一个Stack或者Center包含Image就可以了
less
Container(
width: 300,
height: 400,
color: Colors.blue,
child: Stack(
children: [
Image.asset(
"assets/img/ic_file_pdf.png",
fit: BoxFit.cover,
width: 40,
height: 40,
)
],
),
)
因为默认情况下Stack传递的是宽松约束,可以找到Stack对应的RenderObject:RenderStack,看一下其performLayout是传递了什么约束给子组件
ini
void performLayout() {
.....
size = _computeSize(
constraints: constraints,
layoutChild: ChildLayoutHelper.layoutChild,
);
......
}
Size _computeSize({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
......
//默认情况Stack是StackFit.loose情况,传递是宽松约束
final BoxConstraints nonPositionedConstraints = switch (fit) {
StackFit.loose => constraints.loosen(),
StackFit.expand => BoxConstraints.tight(constraints.biggest),
StackFit.passthrough => constraints,
};
RenderBox? child = firstChild;
while (child != null) {
final StackParentData childParentData = child.parentData! as StackParentData;
if (!childParentData.isPositioned) {
//把nonPositionedConstraints传递给子组件进行布局
final Size childSize = layoutChild(child, nonPositionedConstraints);
}
}
....
return size;
}
上面的代码很明显默认情况下传递的是宽松约束,所以子组件自己设置的宽高是生效的
一些tips
在找某个widget对应的RenderObject一般是这样去找
- 如果widget是RenderObjectWidget,那么直接找到其createRenderObject方法即可
- 如果是StatefulWidget或者StatelessWidget,找到其build方法返回的RenderObjectWidget,再去找对应的RenderObject
这个过程稍微有点麻烦,而且像Container这种在build的时候,会包好几层。
其实在FlutterInspector里面就能直接看到运行时widget树,和我们代码里面对应的widget多出了一些东西

总结
本文从一个布局问题,探索flutter的布局流程,再提出约束这个概念,进而引申出严格约束和宽松约束两种分类,再结合布局流程将问题的一般排查问题的手段定位到performLayout和computeDryLayout这两个具体方法里面,不同的组件有不同的实现,进而解决对应的问题