跳到主要内容

二、布局控件原理及约束

布局控件简介

布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排列(layout)方式不同: 为什么不是Text这种控件? 我理解Text只是RenderObject的一个最终实现,可以查看: 2.2 Widget 简介 | 《Flutter实战·第二版》

Widget说明用途
LeafRenderObjectWidget非容器类组件基类Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。
SingleChildRenderObjectWidget单子组件基类包含一个子Widget,如:ConstrainedBox、DecoratedBox等
MultiChildRenderObjectWidget多子组件基类包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等

布局原理与约束(constraints)

尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio 等

Flutter布局流程如下:

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

    父组件传递给子组件的约束是“最大宽高不能超过100,最小宽高为0”,如果我们给子组件设置宽高都为200,则子组件最终的大小是100*100,因为任何时候子组件都必须先遵守父组件的约束,在此基础上再应用子组件约束(相当于父组件的约束和自身的大小求一个交集)。

盒模型布局组件有两个特点:

  1. 组件对应的渲染对象都继承自 RenderBox 类。在本书后面文章中如果提到某个组件是 RenderBox,则指它是基于盒模型布局的,而不是说组件是 RenderBox 类的实例。
  2. 在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。

BoxConstraints

BoxConstraints 是盒模型布局过程中父渲染对象传递给子渲染对象的约束信息,包含最大宽高信息,子组件大小需要在约束的范围内,BoxConstraints 默认的构造函数如下:

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

BoxConstraints还定义了一些便捷的构造函数用于快速生成特定限制规则的BoxConstraints,如:

  • BoxConstraints.tight(Size size),它可以生成固定宽高的限制;
  • BoxConstraints.expand()可以生成一个尽可能大的用以填充另一个容器的BoxConstraints。

ConstrainedBox

class Home extends StatelessWidget {
const Home({super.key}); // 将构造函数改为常量构造函数

final Widget redBox = const DecoratedBox( // 红色盒子不指定高度
decoration: BoxDecoration(color: Colors.red),
);


// demo1 文本控件
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'布局',
),
centerTitle: true, // 文本居中
backgroundColor: Colors.cyanAccent.shade700,
),
body: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: double.infinity, //宽度尽可能大
minHeight: 50.0 //最小高度为50像素
),
child: Container(
height: 1.0,
child: redBox,
),
),
);
}
}

![](../../static/docs/Pasted image 20240130182655.jpeg) 在这个例子中,ConstrainedBox 强制其子widget至少有 50.0 的高度,但 Container widget的高度被设置为 1.0,违反了这个约束。 当发生这种约束冲突时,Flutter会尝试解决这个问题,通常是通过忽略掉一些约束来避免布局失败。在这种情况下,Container 的实际高度将会是 50.0,而不是 1.0,因为 ConstrainedBox 的约束优先级更高。

SizedBox

实际上SizedBox只是ConstrainedBox的一个定制,

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

实际上ConstrainedBoxSizedBox都是通过RenderConstrainedBox来渲染的,我们可以看到ConstrainedBoxSizedBoxcreateRenderObject()方法都返回的是一个RenderConstrainedBox对象

多重限制

如果某一个组件有多个父级ConstrainedBox限制:

ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
  • 最小宽度和高度 (minWidth, minHeight): 子Widget的最小尺寸会取决于它自身的限制和父Widget限制中较大的值。
  • 最大宽度和高度 (maxWidth, maxHeight): 子Widget的最大尺寸会取决于它自身的限制和父Widget限制中较小的值。

UnconstrainedBox

UnconstrainedBox是Flutter中的一个布局widget,它对其子widget不施加任何约束,这意味着子widget可以根据其自身大小任意绘制,而不受父widget的限制。这在某些布局场景中非常有用,特别是想要让子widget完全按照其自身的大小需求来布局时。

UnconstrainedBox对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight(100.0)仍然是生效的,只不过它不影响最终子元素redBox的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox是作用于子UnconstrainedBox上,而redBox只受子ConstrainedBox限制。 那么有什么方法可以彻底去除父ConstrainedBox的限制吗?答案是否定的!请牢记,任何时候子组件都必须遵守其父组件的约束,所以在此提示读者,在定义一个通用的组件时,如果要对子组件指定约束,那么一定要注意,因为一旦指定约束条件,子组件自身就不能违反约束。

注意事项

虽然UnconstrainedBox提供了布局的灵活性,但也应谨慎使用,因为它可能导致以下问题:

  • 布局溢出:如果UnconstrainedBox的子widget超出了可视区域,可能会出现布局溢出的情况,Flutter会在界面上显示溢出警告(黄黑相间的斜条)。
  • 性能问题:在某些情况下,如果UnconstrainedBox内部的widget非常复杂,且尺寸很大,可能会对性能产生负面影响。

在实际开发中,当我们发现已经使用 SizedBox 或 ConstrainedBox给子元素指定了固定宽高,但是仍然没有效果时,几乎可以断定:已经有父组件指定了约束!举个例子,如 Material 组件库中的AppBar(导航栏)的右侧菜单会限制子组件的宽高,所以可以通过UnconstrainedBox去除限制:

AppBar(
title: Text(title),
actions: <Widget>[
UnconstrainedBox(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
)
],
)