理解约束
当学习 Flutter 的人问你为什么某些带有 width: 100
的小部件宽度不是 100 像素时, 默认答案是告诉他们将该小部件放在 Center
内,对吧?
不要这样做。
如果你这样做,他们会一次又一次地回来, 问为什么某些 FittedBox
不起作用, 为什么那个 Column
溢出了,或者 IntrinsicWidth
应该做什么。
相反,首先告诉他们 Flutter 布局与 HTML 布局(这可能是他们的来源)非常不同, 然后让他们记住以下规则:
如果不了解此规则,就无法真正理解 Flutter 布局,因此 Flutter 开发人员应该尽早学习它。
更详细地说:
- 小部件从其 父级 获取自己的 约束 。 约束 只是一组 4 个双精度数: 最小宽度和最大宽度,以及最小高度和最大高度。
- 然后,小部件遍历其自己的 子级 列表。 小部件逐个告诉其子级它们的 约束是什么(每个子级的约束可能不同), 然后询问每个子级想要多大。
- 然后,小部件逐个定位其子级 (水平方向在
x
轴上,垂直方向在y
轴上)。 - 最后,小部件告诉其父级其自身的尺寸 (当然,在原始约束范围内)。
例如,如果一个组合小部件包含一个带有某些填充的列,并且想要如下布局其两个子级:
![视觉布局](/assets/images/docs/ui/layout/children.png)
协商过程如下:
小部件:“嘿,父级,我的约束是什么?”
父级:“你的宽度必须在 0
到 300
像素之间, 高度必须在 0
到 85
之间。”
小部件:“嗯,由于我想有 5
像素的填充, 那么我的子级的宽度最多可以有 290
像素, 高度最多可以有 75
像素。”
小部件:“嘿,第一个子级,你的宽度必须在 0
到 290
像素之间,高度必须在 0
到 75
之间。”
第一个子级:“好的,那么我希望宽度为 290
像素, 高度为 20
像素。”
小部件:“嗯,由于我想把我的第二个子级放在第一个子级下面, 这只能为我的第二个子级留下 55
像素的高度。”
小部件:“嘿,第二个子级,你的宽度必须在 0
到 290
之间, 高度必须在 0
到 55
之间。”
第二个子级:“好的,我希望宽度为 140
像素, 高度为 30
像素。”
小部件:“很好。我的第一个子级的 x
坐标为 5
,y
坐标为 5
, 我的第二个子级的 x
坐标为 80
,y
坐标为 25
。”
小部件:“嘿,父级,我已经决定我的尺寸将是 300
像素宽,60
像素高。”
限制
#Flutter 的布局引擎设计为单遍过程。 这意味着 Flutter 布局其小部件非常高效, 但这确实导致了一些限制:
小部件只能在其父级给定的约束范围内决定其自身的大小。 这意味着小部件通常 不能具有任何想要的大小。
小部件 无法知道也无法决定其在屏幕上的位置 , 因为是小部件的父级决定小部件的位置。
由于父级的大小和位置又取决于其自身的父级, 因此如果不考虑整个树,就无法精确地定义任何小部件的大小和位置。
如果子级想要的大小与其父级不同,并且 父级没有足够的信息来对其进行对齐, 那么子级的大小可能会被忽略。 定义对齐时要具体。
在 Flutter 中,小部件由其底层的 RenderBox
对象呈现。Flutter 中的许多框, 尤其是那些只接受一个子级的框, 会将其约束传递给其子级。
一般来说,就它们如何处理其约束而言,有三种类型的框:
- 那些试图尽可能大的框。 例如,
Center
和ListView
使用的框。 - 那些试图与其子级大小相同的框。 例如,
Transform
和Opacity
使用的框。 - 那些试图达到特定大小的框。 例如,
Image
和Text
使用的框。
某些小部件,例如 Container
, 根据其构造函数参数而异。Container
构造函数默认 尝试尽可能大,但如果您为其指定 width
, 例如,它会尝试遵守该值并达到该特定大小。
其他小部件,例如 Row
和 Column
(弹性框) 根据它们获得的约束而异, 如“Flex”部分所述。
示例
#为了获得交互式体验,请使用以下 DartPad。 使用编号的水平滚动条在 29 个不同的示例之间切换。
import 'package:flutter/material.dart';
void main() => runApp(const HomePage());
const red = Colors.red;
const green = Colors.green;
const blue = Colors.blue;
const big = TextStyle(fontSize: 30);
//////////////////////////////////////////////////
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return const FlutterLayoutArticle([
Example1(),
Example2(),
Example3(),
Example4(),
Example5(),
Example6(),
Example7(),
Example8(),
Example9(),
Example10(),
Example11(),
Example12(),
Example13(),
Example14(),
Example15(),
Example16(),
Example17(),
Example18(),
Example19(),
Example20(),
Example21(),
Example22(),
Example23(),
Example24(),
Example25(),
Example26(),
Example27(),
Example28(),
Example29(),
]);
}
}
//////////////////////////////////////////////////
abstract class Example extends StatelessWidget {
const Example({super.key});
String get code;
String get explanation;
}
//////////////////////////////////////////////////
class FlutterLayoutArticle extends StatefulWidget {
const FlutterLayoutArticle(
this.examples, {
super.key,
});
final List<Example> examples;
@override
State<FlutterLayoutArticle> createState() => _FlutterLayoutArticleState();
}
//////////////////////////////////////////////////
class _FlutterLayoutArticleState extends State<FlutterLayoutArticle> {
late int count;
late Widget example;
late String code;
late String explanation;
@override
void initState() {
count = 1;
code = const Example1().code;
explanation = const Example1().explanation;
super.initState();
}
@override
void didUpdateWidget(FlutterLayoutArticle oldWidget) {
super.didUpdateWidget(oldWidget);
var example = widget.examples[count - 1];
code = example.code;
explanation = example.explanation;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Layout Article',
home: SafeArea(
child: Material(
color: Colors.black,
child: FittedBox(
child: Container(
width: 400,
height: 670,
color: const Color(0xFFCCCCCC),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(
width: double.infinity, height: double.infinity),
child: widget.examples[count - 1])),
Container(
height: 50,
width: double.infinity,
color: Colors.black,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (int i = 0; i < widget.examples.length; i++)
Container(
width: 58,
padding: const EdgeInsets.only(left: 4, right: 4),
child: button(i + 1),
),
],
),
),
),
Container(
height: 273,
color: Colors.grey[50],
child: Scrollbar(
child: SingleChildScrollView(
key: ValueKey(count),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Center(child: Text(code)),
const SizedBox(height: 15),
Text(
explanation,
style: TextStyle(
color: Colors.blue[900],
fontStyle: FontStyle.italic),
),
],
),
),
),
),
),
],
),
),
),
),
),
);
}
Widget button(int exampleNumber) {
return Button(
key: ValueKey('button$exampleNumber'),
isSelected: count == exampleNumber,
exampleNumber: exampleNumber,
onPressed: () {
showExample(
exampleNumber,
widget.examples[exampleNumber - 1].code,
widget.examples[exampleNumber - 1].explanation,
);
},
);
}
void showExample(int exampleNumber, String code, String explanation) {
setState(() {
count = exampleNumber;
this.code = code;
this.explanation = explanation;
});
}
}
//////////////////////////////////////////////////
class Button extends StatelessWidget {
final bool isSelected;
final int exampleNumber;
final VoidCallback onPressed;
const Button({
super.key,
required this.isSelected,
required this.exampleNumber,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: isSelected ? Colors.grey : Colors.grey[800],
),
child: Text(exampleNumber.toString()),
onPressed: () {
Scrollable.ensureVisible(
context,
duration: const Duration(milliseconds: 350),
curve: Curves.easeOut,
alignment: 0.5,
);
onPressed();
},
);
}
}
//////////////////////////////////////////////////
class Example1 extends Example {
const Example1({super.key});
@override
final code = 'Container(color: red)';
@override
final explanation = '屏幕是 Container 的父级,'
'它强制 Container 的大小与屏幕完全相同。'
'\n\n'
'因此,Container 填充屏幕并将其涂成红色。';
@override
Widget build(BuildContext context) {
return Container(color: red);
}
}
//////////////////////////////////////////////////
class Example2 extends Example {
const Example2({super.key});
@override
final code = 'Container(width: 100, height: 100, color: red)';
@override
final String explanation =
'红色的 Container 想要成为 100x100,但它做不到,'
'因为屏幕强制它的大小与屏幕完全相同。'
'\n\n'
'所以 Container 填充屏幕。';
@override
Widget build(BuildContext context) {
return Container(width: 100, height: 100, color: red);
}
}
//////////////////////////////////////////////////
class Example3 extends Example {
const Example3({super.key});
@override
final code = 'Center(\n'
' child: Container(width: 100, height: 100, color: red))';
@override
final String explanation =
'屏幕强制 Center 的大小与屏幕完全相同,'
'因此 Center 填充屏幕。'
'\n\n'
'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
'现在 Container 确实可以是 100x100。';
@override
Widget build(BuildContext context) {
return Center(
child: Container(width: 100, height: 100, color: red),
);
}
}
//////////////////////////////////////////////////
class Example4 extends Example {
const Example4({super.key});
@override
final code = 'Align(\n'
' alignment: Alignment.bottomRight,\n'
' child: Container(width: 100, height: 100, color: red))';
@override
final String explanation =
'这与之前的示例不同,因为它使用 Align 而不是 Center。'
'\n\n'
'Align 也告诉 Container 它可以是任何大小,但如果有空隙,它不会将 Container 居中。'
'相反,它将 Container 对齐到可用空间的右下角。';
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
);
}
}
//////////////////////////////////////////////////
class Example5 extends Example {
const Example5({super.key});
@override
final code = 'Center(\n'
' child: Container(\n'
' color: red,\n'
' width: double.infinity,\n'
' height: double.infinity))';
@override
final String explanation =
'屏幕强制 Center 的大小与屏幕完全相同,'
'因此 Center 填充屏幕。'
'\n\n'
'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
'Container 想要无限大,但由于它不能大于屏幕,因此它只填充屏幕。';
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: double.infinity, height: double.infinity, color: red),
);
}
}
//////////////////////////////////////////////////
class Example6 extends Example {
const Example6({super.key});
@override
final code = 'Center(child: Container(color: red))';
@override
final String explanation =
'屏幕强制 Center 的大小与屏幕完全相同,'
'因此 Center 填充屏幕。'
'\n\n'
'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
'\n\n'
'由于 Container 没有子级且没有固定大小,它决定它想要尽可能大,因此它填充整个屏幕。'
'\n\n'
'但是为什么 Container 会这样决定呢?'
'仅仅是因为创建 Container 小部件的人的设计决定。'
'它本来可以被不同地创建,您必须阅读 Container 文档才能理解它在不同情况下如何表现。';
@override
Widget build(BuildContext context) {
return Center(
child: Container(color: red),
);
}
}
//////////////////////////////////////////////////
class Example7 extends Example {
const Example7({super.key});
@override
final code = 'Center(\n'
' child: Container(color: red\n'
' child: Container(color: green, width: 30, height: 30)))';
@override
final String explanation =
'屏幕强制 Center 的大小与屏幕完全相同,'
'因此 Center 填充屏幕。'
'\n\n'
'Center 告诉红色 Container 它可以是任何大小,但不能大于屏幕。'
'由于红色 Container 没有大小但有子级,它决定它想要与其子级大小相同。'
'\n\n'
'红色 Container 告诉其子级它可以是任何大小,但不能大于屏幕。'
'\n\n'
'子级是一个想要成为 30x30 的绿色 Container。'
'\n\n'
'由于红色 `Container` 没有大小但有子级,它决定它想要与其子级大小相同。'
'红色不可见,因为绿色 Container 完全覆盖了红色 Container。';
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
);
}
}
//////////////////////////////////////////////////
class Example8 extends Example {
const Example8({super.key});
@override
final code = 'Center(\n'
' child: Container(color: red\n'
' padding: const EdgeInsets.all(20),\n'
' child: Container(color: green, width: 30, height: 30)))';
@override
final String explanation =
'红色 Container 的大小与其子级大小相同,但它考虑了自己的填充。'
'所以它也是 30x30 加上填充。'
'红色可见是因为填充,绿色 Container 的大小与之前的示例相同。';
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
);
}
}
//////////////////////////////////////////////////
class Example9 extends Example {
const Example9({super.key});
@override
final code = 'ConstrainedBox(\n'
' constraints: BoxConstraints(\n'
' minWidth: 70, minHeight: 70,\n'
' maxWidth: 150, maxHeight: 150),\n'
' child: Container(color: red, width: 10, height: 10)))';
@override
final String explanation =
'您可能会猜想 Container 的大小必须在 70 到 150 像素之间,但您错了。'
'ConstrainedBox 只会从其父级接收到的约束中施加附加约束。'
'\n\n'
'在这里,屏幕强制 ConstrainedBox 的大小与屏幕完全相同,'
'因此它告诉其子级 Container 也假设屏幕的大小,'
'从而忽略其“constraints”参数。';
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
);
}
}
//////////////////////////////////////////////////
class Example10 extends Example {
const Example10({super.key});
@override
final code = 'Center(\n'
' child: ConstrainedBox(\n'
' constraints: BoxConstraints(\n'
' minWidth: 70, minHeight: 70,\n'
' maxWidth: 150, maxHeight: 150),\n'
' child: Container(color: red, width: 10, height: 10))))';
@override
final String explanation =
'现在,Center 允许 ConstrainedBox 的大小达到屏幕大小。'
'\n\n'
'ConstrainedBox 从其“constraints”参数向其子级施加附加约束。'
'\n\n'
'Container 的大小必须在 70 到 150 像素之间。它想要 10 像素,所以它最终会得到 70(最小值)。';
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
),
);
}
}
//////////////////////////////////////////////////
class Example11 extends Example {
const Example11({super.key});
@override
final code = 'Center(\n'
' child: ConstrainedBox(\n'
' constraints: BoxConstraints(\n'
' minWidth: 70, minHeight: 70,\n'
' maxWidth: 150, maxHeight: 150),\n'
' child: Container(color: red, width: 1000, height: 1000))))';
@override
final String explanation =
'Center 允许 ConstrainedBox 的大小达到屏幕大小。'
'ConstrainedBox 从其“constraints”参数向其子级施加附加约束'
'\n\n'
'Container 的大小必须在 70 到 150 像素之间。它想要 1000 像素,所以它最终会得到 150(最大值)。';
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 1000, height: 1000),
),
);
}
}
//////////////////////////////////////////////////
class Example12 extends Example {
const Example12({super.key});
@override
final code = 'Center(\n'
' child: ConstrainedBox(\n'
' constraints: BoxConstraints(\n'
' minWidth: 70, minHeight: 70,\n'
' maxWidth: 150, maxHeight: 150),\n'
' child: Container(color: red, width: 100, height: 100))))';
@override
final String explanation =
'Center 允许 ConstrainedBox 的大小达到屏幕大小。'
'ConstrainedBox 从其“constraints”参数向其子级施加附加约束。'
'\n\n'
'Container 的大小必须在 70 到 150 像素之间。它想要 100 像素,这就是它的大小,因为这在 70 和 150 之间。';
@override
Widget build(BuildContext context) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 100, height: 100),
),
);
}
}
//////////////////////////////////////////////////
class Example13 extends Example {
const Example13({super.key});
@override
final code = 'UnconstrainedBox(\n'
' child: Container(color: red, width: 20, height: 50));';
@override
final String explanation =
'屏幕强制 UnconstrainedBox 的大小与屏幕完全相同。'
'但是,UnconstrainedBox 允许其子级 Container 的大小为任意大小。';
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
);
}
}
//////////////////////////////////////////////////
class Example14 extends Example {
const Example14({super.key});
@override
final code = 'UnconstrainedBox(\n'
' child: Container(color: red, width: 4000, height: 50));';
@override
final String explanation =
'屏幕强制 UnconstrainedBox 的大小与屏幕完全相同,'
'而 UnconstrainedBox 允许其子级 Container 的大小为任意大小。'
'\n\n'
'不幸的是,在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 UnconstrainedBox 中,'
'因此 UnconstrainedBox 显示了令人讨厌的“溢出警告”。';
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
);
}
}
//////////////////////////////////////////////////
class Example15 extends Example {
const Example15({super.key});
@override
final code = 'OverflowBox(\n'
' minWidth: 0,'
' minHeight: 0,'
' maxWidth: double.infinity,'
' maxHeight: double.infinity,'
' child: Container(color: red, width: 4000, height: 50));';
@override
final String explanation =
'屏幕强制 OverflowBox 的大小与屏幕完全相同,'
'而 OverflowBox 允许其子级 Container 的大小为任意大小。'
'\n\n'
'OverflowBox 类似于 UnconstrainedBox,区别在于如果子级不适合空间,它不会显示任何警告。'
'\n\n'
'在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 OverflowBox 中,'
'但 OverflowBox 只显示它所能显示的内容,没有任何警告。';
@override
Widget build(BuildContext context) {
return OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
);
}
}
//////////////////////////////////////////////////
class Example16 extends Example {
const Example16({super.key});
@override
final code = 'UnconstrainedBox(\n'
' child: Container(color: Colors.red, width: double.infinity, height: 100));';
@override
final String explanation =
'这不会渲染任何内容,您将在控制台中看到错误。'
'\n\n'
'UnconstrainedBox 允许其子级的大小为任意大小,'
'但是其子级是具有无限大小的 Container。'
'\n\n'
'Flutter 无法渲染无限大小,因此它会抛出一个错误,错误消息如下:'
'"BoxConstraints 强制无限宽度。"';
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
);
}
}
//////////////////////////////////////////////////
class Example17 extends Example {
const Example17({super.key});
@override
final code = 'UnconstrainedBox(\n'
' child: LimitedBox(maxWidth: 100,\n'
' child: Container(color: Colors.red,\n'
' width: double.infinity, height: 100));';
@override
final String explanation = '在这里,您将不再收到错误,'
'因为当 UnconstrainedBox 为 LimitedBox 提供无限大小时,'
'它会向下传递 100 的最大宽度给其子级。'
'\n\n'
'如果您将 UnconstrainedBox 与 Center 小部件互换,'
'LimitedBox 将不再应用其限制(因为其限制仅在其获得无限约束时应用),'
'并且 Container 的宽度允许超过 100。'
'\n\n'
'这解释了 LimitedBox 和 ConstrainedBox 之间的区别。';
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
);
}
}
//////////////////////////////////////////////////
class Example18 extends Example {
const Example18({super.key});
@override
final code = 'FittedBox(\n'
' child: Text(\'Some Example Text.\'));';
@override
final String explanation =
'屏幕强制 FittedBox 的大小与屏幕完全相同。'
'Text 有一些自然宽度(也称为其内在宽度),这取决于文本量、字体大小等。'
'\n\n'
'FittedBox 允许 Text 的大小为任意大小,'
'但在 Text 将其大小告诉 FittedBox 后,'
'FittedBox 会缩放 Text,直到它填满所有可用宽度。';
@override
Widget build(BuildContext context) {
return const FittedBox(
child: Text('Some Example Text.'),
);
}
}
//////////////////////////////////////////////////
class Example19 extends Example {
const Example19({super.key});
@override
final code = 'Center(\n'
' child: FittedBox(\n'
' child: Text(\'Some Example Text.\')));';
@override
final String explanation =
'但是,如果您将 FittedBox 放入 Center 小部件中会发生什么?'
'Center 允许 FittedBox 的大小达到屏幕大小。'
'\n\n'
'然后,FittedBox 将自身大小调整为 Text,并允许 Text 的大小为任意大小。'
'\n\n'
'由于 FittedBox 和 Text 的大小相同,因此不会发生缩放。';
@override
Widget build(BuildContext context) {
return const Center(
child: FittedBox(
child: Text('Some Example Text.'),
),
);
}
}
////////////////////////////////////////////////////
class Example20 extends Example {
const Example20({super.key});
@override
final code = 'Center(\n'
' child: FittedBox(\n'
' child: Text(\'…\')));';
@override
final String explanation =
'但是,如果 FittedBox 在 Center 小部件内,但 Text 太大而无法适应屏幕会发生什么?'
'\n\n'
'FittedBox 试图将其自身大小调整为 Text,但它不能大于屏幕。'
'然后它假设屏幕大小,并调整 Text 的大小,使其也适合屏幕。';
@override
Widget build(BuildContext context) {
return const Center(
child: FittedBox(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.'),
),
);
}
}
//////////////////////////////////////////////////
class Example21 extends Example {
const Example21({super.key});
@override
final code = 'Center(\n'
' child: Text(\'…\'));';
@override
final String explanation = '但是,如果您移除 FittedBox,'
'Text 从屏幕获取其最大宽度,'
'并换行以适应屏幕。';
@override
Widget build(BuildContext context) {
return const Center(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.'),
);
}
}
//////////////////////////////////////////////////
class Example22 extends Example {
const Example22({super.key});
@override
final code = 'FittedBox(\n'
' 如果您愿意,可以从
[这个 GitHub 仓库](https://github.com/marcglasberg/flutter_layout_article) 获取代码。
以下章节解释了这些示例。
[这个 GitHub 仓库]: https://github.com/marcglasberg/flutter_layout_article
### 示例 1
<img src='/assets/images/docs/ui/layout/layout-1.png' class="mw-100" alt="示例 1 布局">
<?code-excerpt "lib/main.dart (Example1)" replace="/(return |;)//g"?>
```dart
Container(color: red)
屏幕是 Container
的父级,它强制 Container
的大小与屏幕完全相同。
因此,Container
填充屏幕并将其涂成红色。
示例 2
#![示例 2 布局](/assets/images/docs/ui/layout/layout-2.png)
Container(width: 100, height: 100, color: red)
红色的 Container
想要成为 100 × 100, 但它做不到,因为屏幕强制它的大小与屏幕完全相同。
因此,Container
填充屏幕。
示例 3
#![示例 3 布局](/assets/images/docs/ui/layout/layout-3.png)
Center(
child: Container(width: 100, height: 100, color: red),
)
屏幕强制 Center
的大小与屏幕完全相同,因此 Center
填充屏幕。
Center
告诉 Container
它可以是任何大小,但不能大于屏幕。现在 Container
确实可以是 100 × 100。
示例 4
#![示例 4 布局](/assets/images/docs/ui/layout/layout-4.png)
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
这与之前的示例不同,因为它使用 Align
而不是 Center
。
Align
也告诉 Container
它可以是任何大小,但如果有空隙,它不会将 Container
居中。 相反,它将容器对齐到可用空间的右下角。
示例 5
#![示例 5 布局](/assets/images/docs/ui/layout/layout-5.png)
Center(
child: Container(
width: double.infinity, height: double.infinity, color: red),
)
屏幕强制 Center
的大小与屏幕完全相同,因此 Center
填充屏幕。
Center
告诉 Container
它可以是任何大小,但不能大于屏幕。Container
想要无限大,但由于它不能大于屏幕,因此它只填充屏幕。
示例 6
#![示例 6 布局](/assets/images/docs/ui/layout/layout-6.png)
Center(
child: Container(color: red),
)
屏幕强制 Center
的大小与屏幕完全相同,因此 Center
填充屏幕。
Center
告诉 Container
它可以是任何大小,但不能大于屏幕。 由于 Container
没有子级且没有固定大小, 它决定它想要尽可能大, 因此它填充整个屏幕。
但是为什么 Container
会这样决定呢? 仅仅是因为创建 Container
小部件的人的设计决定。它本来可以被不同地创建,您必须阅读 Container
的 API 文档才能理解它在不同情况下如何表现。
示例 7
#![示例 7 布局](/assets/images/docs/ui/layout/layout-7.png)
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
屏幕强制 Center
的大小与屏幕完全相同,因此 Center
填充屏幕。
Center
告诉红色的 Container
它可以是任何大小,但不能大于屏幕。由于红色的 Container
没有大小但有子级,它决定它想要与其子级大小相同。
红色的 Container
告诉其子级它可以是任何大小,但不能大于屏幕。
子级是一个想要成为 30 × 30 的绿色 Container
。鉴于红色的 Container
将自身大小调整为其子级的大小,它也是 30 × 30。 红色不可见,因为绿色 Container
完全覆盖了红色的 Container
。
示例 8
#![示例 8 布局](/assets/images/docs/ui/layout/layout-8.png)
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
红色的 Container
将自身大小调整为其子级的大小, 但它考虑了自己的填充。 所以它也是 30 × 30 加上填充。 红色可见是因为填充, 而绿色的 Container
与之前的示例大小相同。
示例 9
#![示例 9 布局](/assets/images/docs/ui/layout/layout-9.png)
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
)
您可能会猜想 Container
的大小必须在 70 到 150 像素之间,但您错了。 ConstrainedBox
只会从其父级接收到的约束中施加 额外 的约束。
在这里,屏幕强制 ConstrainedBox
的大小与屏幕完全相同,因此它告诉其子级 Container
也假设屏幕的大小,从而忽略其 constraints
参数。
示例 10
#![示例 10 布局](/assets/images/docs/ui/layout/layout-10.png)
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
),
)
现在,Center
允许 ConstrainedBox
的大小达到屏幕大小。ConstrainedBox
从其 constraints
参数向其子级施加 额外 的约束。
Container
的大小必须在 70 到 150 像素之间。它想要 10 像素,所以它最终会得到 70(最小值)。
示例 11
#![示例 11 布局](/assets/images/docs/ui/layout/layout-11.png)
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 1000, height: 1000),
),
)
Center
允许 ConstrainedBox
的大小达到屏幕大小。ConstrainedBox
从其 constraints
参数向其子级施加 额外 的约束。
Container
的大小必须在 70 到 150 像素之间。它想要 1000 像素,所以它最终会得到 150(最大值)。
示例 12
#![示例 12 布局](/assets/images/docs/ui/layout/layout-12.png)
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 100, height: 100),
),
)
Center
允许 ConstrainedBox
的大小达到屏幕大小。ConstrainedBox
从其 constraints
参数向其子级施加 额外 的约束。
Container
的大小必须在 70 到 150 像素之间。它想要 100 像素,这就是它的大小,因为这在 70 和 150 之间。
示例 13
#![示例 13 布局](/assets/images/docs/ui/layout/layout-13.png)
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
屏幕强制 UnconstrainedBox
的大小与屏幕完全相同。但是,UnconstrainedBox
允许其子级 Container
的大小为任意大小。
示例 14
#![示例 14 布局](/assets/images/docs/ui/layout/layout-14.png)
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox
的大小与屏幕完全相同,而 UnconstrainedBox
允许其子级 Container
的大小为任意大小。
不幸的是,在这种情况下,Container
的宽度为 4000 像素,并且太大而无法放入 UnconstrainedBox
中,因此 UnconstrainedBox
显示了令人讨厌的“溢出警告”。
示例 15
#![示例 15 布局](/assets/images/docs/ui/layout/layout-15.png)
OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
)
屏幕强制 OverflowBox
的大小与屏幕完全相同,而 OverflowBox
允许其子级 Container
的大小为任意大小。
OverflowBox
类似于 UnconstrainedBox
; 区别在于,如果子级不适合空间,它不会显示任何警告。
在这种情况下,Container
的宽度为 4000 像素,并且太大而无法放入 OverflowBox
中, 但 OverflowBox
只显示它所能显示的内容,没有任何警告。
示例 16
#![示例 16 布局](/assets/images/docs/ui/layout/layout-16.png)
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
这不会渲染任何内容,您将在控制台中看到错误。
UnconstrainedBox
允许其子级的大小为任意大小, 但是其子级是具有无限大小的 Container
。
Flutter 无法渲染无限大小,因此它会抛出一个错误,错误消息如下:BoxConstraints forces an infinite width.
示例 17
#![示例 17 布局](/assets/images/docs/ui/layout/layout-17.png)
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
在这里,您将不再收到错误, 因为当 UnconstrainedBox
为 LimitedBox
提供无限大小时, 它会向下传递 100 的最大宽度给其子级。
如果您将 UnconstrainedBox
与 Center
小部件互换, LimitedBox
将不再应用其限制(因为其限制仅在其获得无限约束时应用),并且 Container
的宽度允许超过 100。
这解释了 LimitedBox
和 ConstrainedBox
之间的区别。
示例 18
#![示例 18 布局](/assets/images/docs/ui/layout/layout-18.png)
const FittedBox(
child: Text('Some Example Text.'),
)
屏幕强制 FittedBox
的大小与屏幕完全相同。Text
有一些自然宽度(也称为其内在宽度),这取决于 文本量、字体大小等。
FittedBox
允许 Text
的大小为任意大小, 但在 Text
将其大小告诉 FittedBox
后, FittedBox
会缩放 Text,直到它填满所有可用宽度。
示例 19
#![示例 19 布局](/assets/images/docs/ui/layout/layout-19.png)
const Center(
child: FittedBox(
child: Text('Some Example Text.'),
),
)
但是,如果您将 FittedBox
放入 Center
小部件中会发生什么?Center
允许 FittedBox
的大小达到屏幕大小。
FittedBox
然后将自身大小调整为 Text
,并允许 Text
的大小为任意大小。 由于 FittedBox
和 Text
的大小相同,因此不会发生缩放。
示例 20
#![示例 20 布局](/assets/images/docs/ui/layout/layout-20.png)
const Center(
child: FittedBox(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.'),
),
)
但是,如果 FittedBox
在 Center
小部件内,但 Text
太大而无法适应屏幕会发生什么?
FittedBox
试图将其自身大小调整为 Text
, 但它不能大于屏幕。 然后它假设屏幕大小, 并调整 Text
的大小,使其也适合屏幕。
示例 21
#![示例 21 布局](/assets/images/docs/ui/layout/layout-21.png)
const Center(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
但是,如果您移除 FittedBox
,Text
将从屏幕获取其最大宽度,并换行以适应屏幕。
示例 22
#![示例 22 布局](/assets/images/docs/ui/layout/layout-22.png)
FittedBox(
child: Container(
height: 20,
width: double.infinity,
color: Colors.red,
),
)
FittedBox
只能缩放边界内的小部件(宽度和高度非无限)。否则,它不会渲染任何内容,您将在控制台中看到错误。
示例 23
#![示例 23 布局](/assets/images/docs/ui/layout/layout-23.png)
Row(
children: [
Container(color: red, child: const Text('Hello!', style: big)),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
屏幕强制 Row
的大小与屏幕完全相同。
就像 UnconstrainedBox
一样,Row
不会对其子级施加任何约束,而是允许它们的大小为任意大小。 然后,Row
将它们并排放置,任何额外的空间都保持为空。
示例 24
#![示例 24 布局](/assets/images/docs/ui/layout/layout-24.png)
Row(
children: [
Container(
color: red,
child: const Text(
'This is a very long text that '
'won\'t fit the line.',
style: big,
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
由于 Row
不会对其子级施加任何约束,因此子级可能太大而无法适应 Row
的可用宽度。在这种情况下,就像 UnconstrainedBox
一样,Row
会显示“溢出警告”。
示例 25
#![示例 25 布局](/assets/images/docs/ui/layout/layout-25.png)
Row(
children: [
Expanded(
child: Center(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
),
Container(color: green, child: const Text('Goodbye!', style: big)),
],
)
当 Row
的子级被 Expanded
小部件包裹时,Row
将不再允许此子级定义其自身宽度。
相反,它会根据其他子级定义 Expanded
的宽度,然后 Expanded
小部件才会强制原始子级具有 Expanded
的宽度。
换句话说,一旦使用 Expanded
,原始子级的宽度就变得无关紧要,并且会被忽略。
示例 26
#![示例 26 布局](/assets/images/docs/ui/layout/layout-26.png)
Row(
children: [
Expanded(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Expanded(
child: Container(
color: green,
child: const Text(
'Goodbye!',
style: big,
),
),
),
],
)
如果所有 Row
的子级都被 Expanded
小部件包裹,则每个 Expanded
的大小与其 flex 参数成比例,然后每个 Expanded
小部件才会强制其子级具有 Expanded
的宽度。
换句话说,Expanded
会忽略其子级的首选宽度。
示例 27
#![示例 27 布局](/assets/images/docs/ui/layout/layout-27.png)
Row(
children: [
Flexible(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Flexible(
child: Container(
color: green,
child: const Text(
'Goodbye!',
style: big,
),
),
),
],
)
如果您使用 Flexible
代替 Expanded
,唯一的区别是 Flexible
允许其子级具有与其自身相同或更小的宽度,而 Expanded
会强制其子级具有与 Expanded
完全相同的宽度。 但 Expanded
和 Flexible
在确定自身大小时都会忽略其子级的宽度。
示例 28
#![示例 28 布局](/assets/images/docs/ui/layout/layout-28.png)
Scaffold(
body: Container(
color: blue,
child: const Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
),
),
)
屏幕强制 Scaffold
的大小与屏幕完全相同,因此 Scaffold
填充屏幕。 Scaffold
告诉 Container
它可以是任何大小,但不能大于屏幕。
示例 29
#![示例 29 布局](/assets/images/docs/ui/layout/layout-29.png)
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
),
),
),
)
如果您希望 Scaffold
的子级大小与其自身完全相同,则可以使用 SizedBox.expand
包裹其子级。
紧凑型约束与宽松型约束
#经常会听到某些约束是“紧凑型”或“宽松型”,那么这意味着什么呢?
紧凑型约束
#紧凑型 约束只提供一种可能性,即精确的大小。换句话说,紧凑型约束的最大宽度等于其最小宽度;最大高度等于其最小高度。
一个例子是 App
小部件,它由 RenderView
类包含:应用程序的 build
函数返回的子级使用的框会收到一个约束,该约束强制它完全填充应用程序的内容区域(通常是整个屏幕)。
另一个例子:如果您在应用程序渲染树的根部嵌套一堆框,它们都将完全彼此契合,这是由框的紧凑型约束强制执行的。
如果您转到 Flutter 的 box.dart
文件并搜索 BoxConstraints
构造函数,您将找到以下内容:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
如果您重新访问示例 2,屏幕会强制红色的 Container
的大小与屏幕完全相同。当然,屏幕是通过向 Container
传递紧凑型约束来实现这一点的。
宽松型约束
#宽松型 约束是指最小值为零而最大值非零的约束。
某些框会 放松 传入的约束,这意味着最大值保持不变,但最小值被移除,因此小部件可以具有 最小 宽度和高度都等于 零 。
最终,Center
的目的是将其从父级(屏幕)接收到的紧凑型约束转换为其子级(Container
)的宽松型约束。
如果您重新访问示例 3,Center
允许红色的 Container
更小,但不允许大于屏幕。
无界约束
#在某些情况下,框的约束是_无界的_ 或无限的。这意味着最大宽度或最大高度被设置为 double.infinity
。
当给出无界约束时,试图尽可能大的框将无法正常工作,并且在调试模式下会抛出异常。
渲染框最终具有无界约束的最常见情况是在弹性框(Row
或 Column
)内,以及在 可滚动区域内 (例如 ListView
和其他 ScrollView
子类)。
例如,ListView
会尝试扩展以适应其交叉方向中可用的空间(也许它是一个垂直滚动的块,并试图与其父级一样宽)。如果您将垂直滚动的 ListView
嵌套在水平滚动的 ListView
内,则内部列表将尝试尽可能宽,这将是无限宽的,因为外部列表在此方向上是可滚动的。
下一节描述了您可能在 Flex
小部件中遇到的无界约束错误。
Flex
#弹性框(Row
和 Column
)的行为不同,这取决于其约束在其主方向上是有界的还是无界的。
主方向上具有界限约束的弹性框将
主方向上试图尽可能大。
主方向上具有无界约束的弹性框试图在其空间中容纳其子级。每个子级的 flex
值必须设置为零,这意味着当弹性框在另一个弹性框或可滚动区域内时,您不能使用Expanded
;否则会抛出异常。
交叉 方向(Column
的宽度或 Row
的高度),绝不 应该无界,否则它就无法合理地对齐其子级。
学习特定小部件的布局规则
#了解一般的布局规则是必要的,但这还不够。
每个小部件在应用一般规则时都有很大的自由度,因此无法仅通过阅读小部件的名称就知道它如何表现。
如果您试图猜测,您很可能会猜错。除非您阅读了其文档或研究了其源代码,否则您无法准确了解小部件的行为方式。
布局源代码通常很复杂,因此最好只阅读文档。但是,如果您决定研究布局源代码,则可以使用 IDE 的导航功能轻松找到它。
以下是一个示例:
在您的代码中找到一个
Column
并导航到其源代码。为此,请在 Android Studio 或 IntelliJ 中使用command+B
(macOS)或control+B
(Windows/Linux)。您将被带到basic.dart
文件。由于Column
扩展了Flex
,因此导航到Flex
源代码(也在basic.dart
中)。向下滚动直到找到名为
createRenderObject()
的方法。如您所见,此方法返回一个RenderFlex
。这是Column
的渲染对象。现在导航到RenderFlex
的源代码,这将带您到flex.dart
文件。向下滚动直到找到名为
performLayout()
的方法。这是为Column
执行布局的方法。
![一个再见布局](/assets/images/docs/ui/layout/layout-final.png)
Marcelo Glasberg 原创文章
Marcelo 最初将此内容发布为 Flutter:即使是初学者也必须知道的进阶布局规则 在 Medium 上。我们非常喜欢它,并请求他允许我们在 docs.flutter.dev 上发布,他欣然同意了。谢谢,Marcelo!您可以在 GitHub 和 pub.dev 上找到 Marcelo。
此外,感谢 Simon Lightfoot 创建文章顶部的标题图像。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。