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<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  }

我们可以看到整个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里是如何布局,绘制的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容