flutter —— 布局原理与约束

一、布局模型

主要有两种布局模型:
① 基于 RenderBox 的盒模型布局。
② 基于 Sliver ( RenderSliver ) 按需加载列表布局。(Sliver 布局请看

两种布局方式在细节上略有差异,但大体流程相同,布局流程如下:

  1. 上层组件向下层组件传递约束(constraints)条件。
  2. 下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
  3. 上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。

二、盒模型的约束

注:若本文中提及到的约束为“无限大”,则表示其最大约束由父级决定。

1 约束模型

const BoxConstraints({
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

严格约束

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

// 等价于
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

2 多重约束

多重约束下的子组件,组件的最小值以父级中最大的为准,最大值以父级中最小的为准。

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 30,
    maxHeight: 180,
  ),
  child: ConstrainedBox(
    constraints: BoxConstraints(maxWidth: 290, maxHeight: 30),
    child: SizedBox(
      width: 550,
      height: 550,
      child: redBox,
    ),
  ),
)

渲染的结果是 30,30 大小的正方形

3 UnconstrainedBox

用于取消父级约束,即子组件将不再受到约束,大小完全取决于自己。

基本原理:假如有一个组件 A,它的子组件是B,B 的子组件是 C,则 C 必须遵守 B 的约束,同时 B 必须遵守 A 的约束,但是 A 的约束不会直接约束到 C,除非B将A对它自己的约束透传给了C。 利用这个原理,就可以实现一个这样的 B 组件。该组件满足下列条件:
① B 组件中在布局 C 时不约束C(可以为无限大)。
② C 根据自身真实的空间占用来确定自身的大小。
③ B 在遵守 A 的约束前提下结合子组件的大小确定自身大小。

通常 UnconstrainedBox 用于取消父组件的最小约束限制。因为 UnconstrainedBox 会随着子组件变大而变大,若 UnconstrainedBox 的大小超过它的父级约束时,就会导致溢出报错。

示例:

代码:

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    //正常模式
    Container(
      color: Colors.blue,
      height: 100,
      width: 100,
    ),
    SizedBox(height: 10),
    //子组件受上级约束影响
    Container(
      color: Colors.blue,
      constraints: BoxConstraints(
        minWidth: 100,
        minHeight: 100,
      ),
      child: Container(
        width: 50,
        height: 50,
        color: Colors.red,
      ),
    ),
    SizedBox(height: 10),
    //通过中间件 UnconstrainedBox 取消约束影响
    Container(
      color: Colors.blue,
      constraints: BoxConstraints(
        minWidth: 100,
        minHeight: 100,
      ),
      child: UnconstrainedBox(
        child: Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
      ),
    ),
    SizedBox(height: 10),
    //通过中间件 Align 取消约束影响
    Container(
      color: Colors.blue,
      constraints: BoxConstraints(
        minWidth: 100,
        minHeight: 100,
      ),
      child: Align(
        heightFactor: 1,
        widthFactor: 1,
        child: Container(
          width: 50,
          height: 50,
          color: Colors.red,
        ),
      ),
    ),
  ],
)

4 OverflowBox

用于对子组件强加额外的约束,且强加的那部分约束可不受父约束限制。相较于 UnconstrainedBox 有两个区别,一是 OverflowBox 的自身大小不随着子组件变化而变化,而是尽可能大,但不超过父约束。二是 OverflowBox 的子组件可超过父级约束,且不报溢出错误(不过溢出部分无法响应事件)。

示例:

代码:

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    //正常模式
    Container(
      color: Colors.blue,
      height: 100,
      width: 100,
    ),
    SizedBox(height: 10),
    //子组件受父级约束影响
    Container(
      color: Colors.blue,
      constraints: BoxConstraints(
        maxWidth: 100,
        maxHeight: 100,
      ),
      child: Container(
        width: 200,
        height: 100,
        color: Colors.red,
      ),
    ),
    SizedBox(height: 10),
    //子组件最大宽度不受父级约束影响
    Container(
      color: Colors.blue,
      constraints: BoxConstraints(
        maxWidth: 100,
        maxHeight: 100,
      ),
      child: OverflowBox(
        maxWidth: double.infinity,
        child: Container(
          width: 200,
          height: 100,
          color: Colors.red,
        ),
      ),
    ),
  ],
)

三、布局原理

例1:自定义 center 组件

class CustomCenter extends SingleChildRenderObjectWidget {
  const CustomCenter({Key? key, required Widget child})
      : super(key: key, child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderCustomCenter();
  }
}

class RenderCustomCenter extends RenderShiftedBox {
  RenderCustomCenter({RenderBox? child}) : super(child);

  @override
  void performLayout() {
    //1. 先对子组件进行layout,随后获取它的size
    child!.layout(
      constraints.loosen(), //将无限制的约束传递给子节点
      parentUsesSize: true, // 因为我们接下来要使用child的size,所以不能为false
    );
    //2.根据子组件的大小确定自身的大小
    size = constraints.constrain(Size(
      //如果父组件的宽度不限制,则使用子组件宽度,否则以父组件的最大宽度为准
      constraints.maxWidth == double.infinity
          ? child!.size.width
          : double.infinity,
      //如果父组件的高度不限制,则使用子组件高度,否则以父组件的最大高度为准
      constraints.maxHeight == double.infinity
          ? child!.size.height
          : double.infinity,
    ));

    // 3. 根据父节点子节点的大小,算出子节点在父节点中居中之后的偏移,然后将这个偏移保存在
    // 子节点的parentData中,在后续的绘制阶段,会用到。
    BoxParentData parentData = child!.parentData as BoxParentData;
    parentData.offset = ((size - child!.size) as Offset) / 2;
  }

  // 注:这部分代码在继承的父类 RenderShiftedBox 中
  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
      // 绘制子节点
      final BoxParentData childParentData = child!.parentData! as BoxParentData;
      context.paintChild(child!, childParentData.offset + offset);
    }
  }
}

例2:Row、Column 线性布局

对于子级组件

以 Row 组件为例,则水平方向不限,垂直方向尽可能大(但不能超出父级约束,如果父级约束不限制,则由自身大小决定)

对于自身

1)纵轴方向

大小来自子级最大的那个,但不能超出父级约束

2)主轴方向

如果主轴方向父级约束是无限大,或 mainAxisSize = MainAxisSize.min(Row,Column 默认此设置为 max),则主轴方向大小为 allocatedSize,即子组件在主轴方向大小的。否则,主轴方向大小为当前父级的最大约束。

示例:

① Row 的默认布局

Container(
  constraints: BoxConstraints(
    maxWidth: 210,
    maxHeight: 220,
  ),
  color: Colors.green,
// 子级约束:宽度不限,高度最大220。自身大小:宽度210,高度102。
  child: Row(
    children: [
      Container(
        width: 301,
        height: 102,
        color: Colors.red,
      ),
      Container(
        width: 50,
        height: 50,
        color: Colors.blue,
      ),
    ],
  ),
)

② Row 设置 主轴 MainAxisSize.min

// 自身大小等于子级中最大的大小,即 50
Row(
  mainAxisSize: MainAxisSize.min,
  children: [
    Container(
      width: 50,
      height: 50,
      color: Colors.blue,
    ),
  ],
)

③ 两个嵌套的 Row

Container(
  constraints: BoxConstraints(
    maxWidth: 210,
    maxHeight: 220,
  ),
  color: Colors.green,
  child: Row(
    children: [
// 内层 Row 的主轴方向上的父级约束不限,所以自身的宽度也是 50
      Row(
        children: [
          Container(
            width: 50,
            height: 50,
            color: Colors.blue,
          ),
        ],
      ),
    ],
  ),
)

④ 两个嵌套的 Row 里面使用 Expanded

Container(
  width: 300,
  height: 300,
  color: Colors.green,
  child: Row(
    children: [
      Row(
        children: [
// 由于内层 Row 的主轴方向上无限制,子组件的 flex 大于 0 会报错(Flexible 或 Expanded 组件的 flex 默认设置为 1
// 报错信息是:RenderFlex children have non-zero flex but incoming width constraints are unbounded.
          Expanded(
            child: Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
          ),
        ],
      )
    ],
  ),
)

例3:OverflowBox 布局原理

待更新

例4:Positioned 布局原理

待更新

例5:Container 布局与 Align

待更新(https://stackoverflow.com/questions/51182803/shrink-container-to-smaller-child-rather-than-expanding-to-fill-parent)

参考文档


https://book.flutterchina.club/chapter14/layout.html#_14-4-1-单子组件布局示例-customcenter

posted on 2022-07-27 11:22  Lemo_wd  阅读(660)  评论(0编辑  收藏  举报

导航