flutter 组件尺寸约束

·  阅读 751

提出问题

在书写 flutter 时是否偶尔发现自己写的宽高没有起作用?其中可能产生的原因就是与 flutter 的默认的约束规则产生了抵触。

demo1

在 flutter 中,如果书写代码

Container(
  width: 100,
  height: 100,
  color: Colors.purple,
  child: Container(
    width: 50,
    height: 50,
    color: Colors.blue,
  ),
);
复制代码

会得到什么呢?我的第一反应就是会得到分别得到一个紫色大方块与蓝色小方块。但是实际上经过 devtool 查看,父子宽高均为100,最终只看到一个蓝色的宽高100的方块。看上去似乎是父组件约束了子组件的宽高。

由此我们知道,父组件可以影响子组件的尺寸

demo2

Container(
  color: Colors.purple,
  child: Container(
    width: 50,
    height: 50,
    color: Colors.blue,
  ),
);
复制代码

这个就比较简单了,我们会得到一个蓝色的宽高50的方块。那么外层 Container 的尺寸是多少呢?通过 flutter devtoolsflutter inspetor 我们可以知道外层 Container 的尺寸也是 50。

由此我们知道,子组件也可以反过来影响父组件的尺寸

疑问小结

我们发现2个知识点看上去是有冲突的,假设有组件嵌套关系如:A-B-C,A 要影响 B 的尺寸,C 也要影响 B 的尺寸,那么到底最终谁说了算?其源码的实现思路又是怎样的呢?

Container(
  width: 100,
  height: 100,
  color: Colors.purple,
  child: Container(
    color: Colors.yellow,
    child: Container(
      width: 50,
      height: 50,
      color: Colors.blue,
    ),
  ),
);
复制代码

中间的 Container 尺寸究竟会是多少呢?

源码解析

Container 的真身

Container 其实是 flutter 中最常见的组件了,但是其实它是一个复合组件。稍微看一下源码就知道了,如下:

// Container
  @override
  Widget build(BuildContext context) {
    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.of(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);

    return current;
  }
复制代码

Container 根据构造函数传入的属性,build 时返回了若干个嵌套的组件。这里我们主要关注处理宽高的地方,查看构造函数我们知道 width , height 属性都被写入 constraints 属性了。而 build 里 constraints != null 就会被丢入 ConstrainedBox 组件。

前置知识——Widget Element RenderObject 的关系

太过庞大,这里不做展开。(或者在某个时空我会补上?)

ConstrainedBox 的真身

查看 ConstrainedBox 的继承链,我们可以发现它继承于 RenderObjectWidget,它是没有 build 方法的,会有一个 renderObject 决定该组件如何绘制。

我们重点关注它覆写的 createRenderObject 方法

@override
RenderConstrainedBox createRenderObject(BuildContext context) {
  return RenderConstrainedBox(additionalConstraints: constraints); // constraints 在构造函数初始化了
}
复制代码

带领我们来到 RenderConstrainedBox,观察其继承链。

其继承于 RenderObject ,如果你阅读过其源码,你会发现注释里面有这么一句话

Subclasses should not override [layout] directly. Instead, they should override [performResize] and/or [performLayout]. The [layout] method delegates the actual work to [performResize] and [performLayout].

RenderConstrainedBox 没有覆写 performResize, 只覆写了 performLayout。那么我们看看其代码实现。

@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  // 根据上方我们的 demo, 可知 child 不会为 null
  if (child != null) {
    // _additionalConstraints 在构造函数中初始化
    child!.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true);
    // 使用子组件的尺寸
    size = child!.size;
  } else {
    size = _additionalConstraints.enforce(constraints).constrain(Size.zero);
  }
}
复制代码

其中 _additionalConstraints.enforce(constraints) 的实现如下

BoxConstraints enforce(BoxConstraints constraints) {
  return BoxConstraints(
    // clamp 是 num 类的一个方法,译为 钳
   // 作用是将当前数值钳在一个范围内,比如 
   // 5.clamp(1, 7) == 5; 5.clamp(10, 20) == 10
    minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth),
    maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth),
    minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight),
    maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight),
  );
}
复制代码

其目的也很好理解了,就是让自身已经存在的 constraints (父级给的)约束住 _additionalConstraints(通过构造函数传入的)。由此我们找到了第一个 demo 的实现,让我们先贴一遍 demo1 的代码。

Container(
  width: 100,
  height: 100,
  color: Colors.purple,
  child: Container(
    width: 50,
    height: 50,
    color: Colors.blue,
  ),
);
复制代码

child.layout 此处不做过多解析。

子级设置的宽高优先级低于父级给到的宽高,因为会被调用 boxConstraints.enfore 钳住。父组件可以影响子组件的尺寸

别忘了调用完 child.layout 后我们还有一句 size = child!.size;,这也符合我们的第二个规则 子组件也可以反过来影响父组件的尺寸

但是第一规则优先级是大于第二规则的,所以有了 demo2 最终的结果——三个 Container 其宽高都为 100。

打破约束

如果基于 demo1 我们就是想要一个宽高100的父级嵌套宽高50的子级怎么办?官方也给我们提供了解决方案,只要给子级嵌套一个 UnconstrainedBox 组件就好了,至于具体的源码,各位可以自己尝试去看看。只要找到其埋藏于底层的 performLayout 就很容易理解了。

总结

flutter 中不少组件隐式的约束了子组件的宽高,比如 PageView,如果发现自己设置的宽高无效,记得使用 UnconstrainedBox

其背后蕴含的原理就是 flutter 的约束规则(实际上有很多种约束规则,本文只讲了最常见的一种)。

分类:
前端
标签:
分类:
前端
标签: