flutter 布局与绘制(上)

趁着假期有时间把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里是如何布局,绘制的。

你可能感兴趣的:(flutter 布局与绘制(上))