趁着假期有时间把flutter布局,绘制相关的内容做个记录。在平时开发中时常会遇到如下的问题:
1.The following assertion was thrown during layout:
A RenderUnconstrainedBox overflowed by 1800 pixels on the left and 1800 pixels on the right.
2.BoxConstraints forces an infinite width.
These invalid constraints were provided to _RenderColoredBox's layout() function by the following function, which probably computed the invalid constraints in question:
RenderConstrainedBox.performLayout (package:flutter/src/rendering/proxy_box.dart:279:14)
3.RenderBox was not laid out: RenderConstrainedBox#cfe5c relayoutBoundary=up1 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1940 pos 12: 'hasSize'等这些跟宽高尺寸相关的异常错误。像这类问题我们往往通过限制宽高,或者在column,row里加上expand貌似就能解决问题,为什么这样能解决可能大部分人都说不清,今天就针对flutter里的布局约束开始,一步步探究下flutter是如何布局的?
先看下面的这段简单代码:
class Example extends StateLessWidget{
const Example({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(width: 100, height: 100, color: red);
}
}
void main(){
runApp(Example());
}
我们只看代码很好理解,就是要显示一个宽高都为100的红色正方形,但是当我们运行的时候发现,整个屏幕都变成了红色。但是我们再Container外部加上Center后发现 宽高就变成了100*100的红色正方形了。
Widget build(BuildContext context) {
return Center(child:Container(width: 100, height: 100, color: red));
}
这到底是是怎么回事呢?一起看看Container的源码吧,找到Container build的方法,
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 (color != null)
current = ColoredBox(color: color!, child: current);
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
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, alignment: transformAlignment);
return current!;
}
很容易会发现,按照我们给Container设置的属性,在build里返回了的应该是ColoredBox,那继续ColoredBox 的源码吧,ColoredBox很简单继承了SingleChildRenderObjectWidget,重写了paint方法
@override
void paint(PaintingContext context, Offset offset) {
// It's tempting to want to optimize out this `drawRect()` call if the
// color is transparent (alpha==0), but doing so would be incorrect. See
// https://github.com/flutter/flutter/pull/72526#issuecomment-749185938 for
// a good description of why.
if (size > Size.zero) {
context.canvas.drawRect(offset & size, Paint()..color = color);
}
if (child != null) {
context.paintChild(child!, offset);
}
}
并有没找到对大小的限制,但是paint方法里的size引起了我的注意。这个size是怎么来的呢?继续紧跟找到colorBox对应的_RenderColoredBox又继续跟进_RenderColoredBox的父类发现在RenderProxyBoxMixin里的performLayout方法,在这里有对size的赋值
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.smallest;
}
由于我们没有给Container设置子widget,所以size的值会是constraints.smallest,那么可以看到child的constraints是从父widget传递过来的。
前面也跟大家分享了启动流程,讲到runAPP里的scheduleAttachRootWidget方法,在这个里面
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement?);
}
我们可以看到整个flutter widget tree最外层是renderView,renderView的构建如下:
void initRenderView() {
assert(!_debugIsRenderViewInitialized);
assert(() {
_debugIsRenderViewInitialized = true;
return true;
}());
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.prepareInitialFrame();
}
renderView的尺寸配置是跟window相关的
ViewConfiguration createViewConfiguration() {
final double devicePixelRatio = window.devicePixelRatio;
return ViewConfiguration(
size: window.physicalSize / devicePixelRatio,
devicePixelRatio: devicePixelRatio,
);
}
从这里不难看出renderView大小就是window的物理尺寸,renderView同样通过performLayout方法将BoxConstraints传递给子Widget。
@override
void performLayout() {
assert(_rootTransform != null);
_size = configuration.size;
assert(_size.isFinite);
if (child != null)
child!.layout(BoxConstraints.tight(_size));
}
到这里我们只是找到了约束的传递,但是并不能完全解释为什么Container就是充满整个屏幕的?所以咱们还得继续看BoxConstraints.tight(_size)到底是啥意思呢?
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
发现直接将子widget的最大最小宽高都约束为了size的宽高,那也就是子widget只能有一个宽,和一个高,从而说明了为什么Container的尺寸是充满屏幕的。
那么为什么加了Center之后尺寸就正常了呢?带着这个疑问我们不得不扒一扒Center的源码。Center是继承自Align的,那咱们直接去找Align的RenderObject的吧,很容易就你能找到RenderPositionedBox,直接看他的performLayout()方法吧
@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) {// 我们把Container设置为他的child,所以会走到这里
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));
}
}
我们需要继续看下center给子Widget传递的约束是 child!.layout(constraints.loosen(), parentUsesSize: true);
constraints.loosen()的代码如下:
BoxConstraints loosen() {
assert(debugAssertIsValid());
return BoxConstraints(
minWidth: 0.0,
maxWidth: maxWidth,
minHeight: 0.0,
maxHeight: maxHeight,
);
}
这样的宽松约束,只要child的宽高在0-max之间就可以。到这里我们可以得到一个结论就是:
上层 widget 向下层 widget 传递约束条件约束child的大小,那么在文章开始提出的几个报错问题实际上都是child没有在约束的限制内,导致出的错。tight严格约束下child的大小是跟父widget传递过来的必须一致,loose宽松约束下,child大小只要在0-最大值的范围内都是没问题的。到这里我相信大家对约束有了一定的了解。想了解更多的伙伴还可以继续到官网去学习https://flutter.cn/docs/development/ui/layout/constraints
深究的小伙伴可能还会在意,父widget 又是如何拿到child的大小并且确定child的位置的呢,父widget的大小又是怎么确定的呢?下一讲一起来看看在flutter里是如何布局,绘制的。