理解约束
Flutter 关于组件约束、尺寸、位置及其相互作用的模型。
当学习 Flutter 的人问你为什么某个 width: 100 的组件不是 100 像素宽时,默认的回答是告诉他们把这个组件放在一个 Center 里,对吧?
不要这样做。
如果你那样做了,他们会一次又一次地回来问你:为什么 FittedBox 不起作用,为什么 Column 会溢出,或者 IntrinsicWidth 到底是做什么的。
相反,首先告诉他们 Flutter 的布局与 HTML 布局(他们可能就是从那里来的)非常不同,然后让他们记住以下规则:
不了解这条规则,就无法真正理解 Flutter 布局,所以 Flutter 开发者应该尽早掌握它。
详细说明
- 组件从其父组件处获取自身的约束。约束只是一组 4 个双精度浮点数:最小宽度和最大宽度,以及最小高度和最大高度。
- 然后,组件会遍历其子组件列表。组件会逐一告诉其子组件它们各自的约束(每个子组件的约束可能不同),并询问每个子组件它想要多大的尺寸。
- 接着,组件会逐一放置其子组件(在
x轴上水平放置,在y轴上垂直放置)。 - 最后,组件会向其父组件报告它自己的尺寸(当然,是在原始约束范围内)。
例如,如果一个复合组件包含一个带有内边距的列,并希望按照以下方式布局其两个子组件:
协商过程大致如下:
组件:“嘿,父组件,我的约束是什么?”
父组件:“你的宽度必须在 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 Color red = Colors.red;
const Color green = Colors.green;
const Color blue = Colors.blue;
const TextStyle 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 =
'The screen is the parent of the Container, '
'and it forces the Container to be exactly the same size as the screen.'
'\n\n'
'So the Container fills the screen and paints it red.';
@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 =
'The red Container wants to be 100x100, but it can\'t, '
'because the screen forces it to be exactly the same size as the screen.'
'\n\n'
'So the Container fills the screen.';
@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 =
'The screen forces the Center to be exactly the same size as the screen, '
'so the Center fills the screen.'
'\n\n'
'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
'Now the Container can indeed be 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 =
'This is different from the previous example in that it uses Align instead of Center.'
'\n\n'
'Align also tells the Container that it can be any size it wants, but if there is empty space it won\'t center the Container. '
'Instead, it aligns the Container to the bottom-right of the available space.';
@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 =
'The screen forces the Center to be exactly the same size as the screen, '
'so the Center fills the screen.'
'\n\n'
'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
'The Container wants to be of infinite size, but since it can\'t be bigger than the screen, it just fills the screen.';
@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 =
'The screen forces the Center to be exactly the same size as the screen, '
'so the Center fills the screen.'
'\n\n'
'The Center tells the Container that it can be any size it wants, but not bigger than the screen.'
'\n\n'
'Since the Container has no child and no fixed size, it decides it wants to be as big as possible, so it fills the whole screen.'
'\n\n'
'But why does the Container decide that? '
'Simply because that\'s a design decision by those who created the Container widget. '
'It could have been created differently, and you have to read the Container documentation to understand how it behaves, depending on the circumstances. ';
@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 =
'The screen forces the Center to be exactly the same size as the screen, '
'so the Center fills the screen.'
'\n\n'
'The Center tells the red Container that it can be any size it wants, but not bigger than the screen.'
'Since the red Container has no size but has a child, it decides it wants to be the same size as its child.'
'\n\n'
'The red Container tells its child that it can be any size it wants, but not bigger than the screen.'
'\n\n'
'The child is a green Container that wants to be 30x30.'
'\n\n'
'Since the red `Container` has no size but has a child, it decides it wants to be the same size as its child. '
'The red color isn\'t visible, since the green Container entirely covers all of the red 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 =
'The red Container sizes itself to its children size, but it takes its own padding into consideration. '
'So it is also 30x30 plus padding. '
'The red color is visible because of the padding, and the green Container has the same size as in the previous example.';
@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 =
'You might guess that the Container has to be between 70 and 150 pixels, but you would be wrong. '
'The ConstrainedBox only imposes ADDITIONAL constraints from those it receives from its parent.'
'\n\n'
'Here, the screen forces the ConstrainedBox to be exactly the same size as the screen, '
'so it tells its child Container to also assume the size of the screen, '
'thus ignoring its \'constraints\' parameter.';
@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 =
'Now, Center allows ConstrainedBox to be any size up to the screen size.'
'\n\n'
'The ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child.'
'\n\n'
'The Container must be between 70 and 150 pixels. It wants to have 10 pixels, so it will end up having 70 (the MINIMUM).';
@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 allows ConstrainedBox to be any size up to the screen size.'
'The ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child'
'\n\n'
'The Container must be between 70 and 150 pixels. It wants to have 1000 pixels, so it ends up having 150 (the MAXIMUM).';
@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 allows ConstrainedBox to be any size up to the screen size.'
'ConstrainedBox imposes ADDITIONAL constraints from its \'constraints\' parameter onto its child.'
'\n\n'
'The Container must be between 70 and 150 pixels. It wants to have 100 pixels, and that\'s the size it has, since that\'s between 70 and 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 =
'The screen forces the UnconstrainedBox to be exactly the same size as the screen.'
'However, the UnconstrainedBox lets its child Container be any size it wants.';
@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 =
'The screen forces the UnconstrainedBox to be exactly the same size as the screen, '
'and UnconstrainedBox lets its child Container be any size it wants.'
'\n\n'
'Unfortunately, in this case the Container has 4000 pixels of width and is too big to fit in the UnconstrainedBox, '
'so the UnconstrainedBox displays the much dreaded "overflow warning".';
@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 =
'The screen forces the OverflowBox to be exactly the same size as the screen, '
'and OverflowBox lets its child Container be any size it wants.'
'\n\n'
'OverflowBox is similar to UnconstrainedBox, and the difference is that it won\'t display any warnings if the child doesn\'t fit the space.'
'\n\n'
'In this case the Container is 4000 pixels wide, and is too big to fit in the OverflowBox, '
'but the OverflowBox simply shows as much as it can, with no warnings given.';
@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 =
'This won\'t render anything, and you\'ll see an error in the console.'
'\n\n'
'The UnconstrainedBox lets its child be any size it wants, '
'however its child is a Container with infinite size.'
'\n\n'
'Flutter can\'t render infinite sizes, so it throws an error with the following message: '
'"BoxConstraints forces an infinite width."';
@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 =
'Here you won\'t get an error anymore, '
'because when the LimitedBox is given an infinite size by the UnconstrainedBox, '
'it passes a maximum width of 100 down to its child.'
'\n\n'
'If you swap the UnconstrainedBox for a Center widget, '
'the LimitedBox won\'t apply its limit anymore (since its limit is only applied when it gets infinite constraints), '
'and the width of the Container is allowed to grow past 100.'
'\n\n'
'This explains the difference between a LimitedBox and a 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 =
'The screen forces the FittedBox to be exactly the same size as the screen.'
'The Text has some natural width (also called its intrinsic width) that depends on the amount of text, its font size, and so on.'
'\n\n'
'The FittedBox lets the Text be any size it wants, '
'but after the Text tells its size to the FittedBox, '
'the FittedBox scales the Text until it fills all of the available width.';
@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 =
'But what happens if you put the FittedBox inside of a Center widget? '
'The Center lets the FittedBox be any size it wants, up to the screen size.'
'\n\n'
'The FittedBox then sizes itself to the Text, and lets the Text be any size it wants.'
'\n\n'
'Since both FittedBox and the Text have the same size, no scaling happens.';
@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 =
'However, what happens if FittedBox is inside of a Center widget, but the Text is too large to fit the screen?'
'\n\n'
'FittedBox tries to size itself to the Text, but it can\'t be bigger than the screen. '
'It then assumes the screen size, and resizes Text so that it fits the screen, too.';
@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 =
'If, however, you remove the FittedBox, '
'the Text gets its maximum width from the screen, '
'and breaks the line so that it fits the screen.';
@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'
' child: Container(\n'
' height: 20, width: double.infinity));';
@override
final String explanation =
'FittedBox can only scale a widget that is BOUNDED (has non-infinite width and height).'
'Otherwise, it won\'t render anything, and you\'ll see an error in the console.';
@override
Widget build(BuildContext context) {
return FittedBox(
child: Container(height: 20, width: double.infinity, color: Colors.red),
);
}
}
//////////////////////////////////////////////////
class Example23 extends Example {
const Example23({super.key});
@override
final code =
'Row(children:[\n'
' Container(color: red, child: Text(\'Hello!\'))\n'
' Container(color: green, child: Text(\'Goodbye!\'))]';
@override
final String explanation =
'The screen forces the Row to be exactly the same size as the screen.'
'\n\n'
'Just like an UnconstrainedBox, the Row won\'t impose any constraints onto its children, '
'and instead lets them be any size they want.'
'\n\n'
'The Row then puts them side-by-side, and any extra space remains empty.';
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
color: red,
child: const Text('Hello!', style: big),
),
Container(
color: green,
child: const Text('Goodbye!', style: big),
),
],
);
}
}
//////////////////////////////////////////////////
class Example24 extends Example {
const Example24({super.key});
@override
final code =
'Row(children:[\n'
' Container(color: red, child: Text(\'…\'))\n'
' Container(color: green, child: Text(\'Goodbye!\'))]';
@override
final String explanation =
'Since the Row won\'t impose any constraints onto its children, '
'it\'s quite possible that the children might be too big to fit the available width of the Row.'
'In this case, just like an UnconstrainedBox, the Row displays the "overflow warning".';
@override
Widget build(BuildContext context) {
return 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),
),
],
);
}
}
//////////////////////////////////////////////////
class Example25 extends Example {
const Example25({super.key});
@override
final code =
'Row(children:[\n'
' Expanded(\n'
' child: Container(color: red, child: Text(\'…\')))\n'
' Container(color: green, child: Text(\'Goodbye!\'))]';
@override
final String explanation =
'When a Row\'s child is wrapped in an Expanded widget, the Row won\'t let this child define its own width anymore.'
'\n\n'
'Instead, it defines the Expanded width according to the other children, and only then the Expanded widget forces the original child to have the Expanded\'s width.'
'\n\n'
'In other words, once you use Expanded, the original child\'s width becomes irrelevant, and is ignored.';
@override
Widget build(BuildContext context) {
return 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),
),
],
);
}
}
//////////////////////////////////////////////////
class Example26 extends Example {
const Example26({super.key});
@override
final code =
'Row(children:[\n'
' Expanded(\n'
' child: Container(color: red, child: Text(\'…\')))\n'
' Expanded(\n'
' child: Container(color: green, child: Text(\'Goodbye!\'))]';
@override
final String explanation =
'If all of Row\'s children are wrapped in Expanded widgets, each Expanded has a size proportional to its flex parameter, '
'and only then each Expanded widget forces its child to have the Expanded\'s width.'
'\n\n'
'In other words, Expanded ignores the preferred width of its children.';
@override
Widget build(BuildContext context) {
return 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),
),
),
],
);
}
}
//////////////////////////////////////////////////
class Example27 extends Example {
const Example27({super.key});
@override
final code =
'Row(children:[\n'
' Flexible(\n'
' child: Container(color: red, child: Text(\'…\')))\n'
' Flexible(\n'
' child: Container(color: green, child: Text(\'Goodbye!\'))]';
@override
final String explanation =
'The only difference if you use Flexible instead of Expanded, '
'is that Flexible lets its child be SMALLER than the Flexible width, '
'while Expanded forces its child to have the same width of the Expanded.'
'\n\n'
'But both Expanded and Flexible ignore their children\'s width when sizing themselves.'
'\n\n'
'This means that it\'s IMPOSSIBLE to expand Row children proportionally to their sizes. '
'The Row either uses the exact child\'s width, or ignores it completely when you use Expanded or Flexible.';
@override
Widget build(BuildContext context) {
return 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),
),
),
],
);
}
}
//////////////////////////////////////////////////
class Example28 extends Example {
const Example28({super.key});
@override
final code =
'Scaffold(\n'
' body: Container(color: blue,\n'
' child: Column(\n'
' children: [\n'
' Text(\'Hello!\'),\n'
' Text(\'Goodbye!\')])))';
@override
final String explanation =
'The screen forces the Scaffold to be exactly the same size as the screen, '
'so the Scaffold fills the screen.'
'\n\n'
'The Scaffold tells the Container that it can be any size it wants, but not bigger than the screen.'
'\n\n'
'When a widget tells its child that it can be smaller than a certain size, '
'we say the widget supplies "loose" constraints to its child. More on that later.';
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
);
}
}
//////////////////////////////////////////////////
class Example29 extends Example {
const Example29({super.key});
@override
final code =
'Scaffold(\n'
' body: Container(color: blue,\n'
' child: SizedBox.expand(\n'
' child: Column(\n'
' children: [\n'
' Text(\'Hello!\'),\n'
' Text(\'Goodbye!\')]))))';
@override
final String explanation =
'If you want the Scaffold\'s child to be exactly the same size as the Scaffold itself, '
'you can wrap its child with SizedBox.expand.'
'\n\n'
'When a widget tells its child that it must be of a certain size, '
'we say the widget supplies "tight" constraints to its child. More on that later.';
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
),
);
}
}
//////////////////////////////////////////////////
如果您愿意,可以从 这个 GitHub 仓库 获取代码。
这些示例将在接下来的部分中进行解释。
示例 1
#
Container(color: red)
屏幕是 Container 的父组件,它强制 Container 的尺寸与屏幕完全相同。
因此,Container 填满了屏幕并将其涂成红色。
示例 2
#
Container(width: 100, height: 100, color: red)
红色的 Container 想要 100 × 100 的尺寸,但它做不到,因为屏幕强制它与屏幕大小完全一致。
因此,Container 填满了屏幕。
示例 3
#
Center(child: Container(width: 100, height: 100, color: red))
屏幕强制 Center 与屏幕大小完全一致,所以 Center 填满了屏幕。
Center 告诉 Container 它可以是它想要的任何尺寸,但不能超过屏幕大小。现在,Container 确实可以变成 100 × 100。
示例 4
#
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
这与上一个示例不同,因为它使用了 Align 而不是 Center。
Align 也告诉 Container 它可以是任何想要的尺寸,但如果有空余空间,它不会居中 Container。相反,它会将容器对齐到可用空间的右下角。
示例 5
#
Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: red,
),
)
屏幕强制 Center 与屏幕大小完全一致,所以 Center 填满了屏幕。
Center 告诉 Container 它可以是任何想要的尺寸,但不能超过屏幕大小。Container 想要无限大的尺寸,但由于它不能超过屏幕大小,所以它只是填满了屏幕。
示例 6
#
Center(child: Container(color: red))
屏幕强制 Center 与屏幕大小完全一致,所以 Center 填满了屏幕。
Center 告诉 Container 它可以是任何想要的尺寸,但不能超过屏幕大小。由于 Container 没有子组件且没有固定尺寸,它决定要尽可能变大,因此填满了整个屏幕。
但 Container 为什么会这样决定呢?很简单,这是创建 Container 组件的设计决策。它可以以不同的方式创建,你必须阅读 Container 的 API 文档,以了解它如何根据环境表现。
示例 7
#
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
屏幕强制 Center 与屏幕大小完全一致,所以 Center 填满了屏幕。
Center 告诉红色 Container 它可以是任何想要的尺寸,但不能超过屏幕大小。由于红色 Container 本身没有尺寸但有一个子组件,它决定要与其子组件的尺寸相同。
红色 Container 告诉其子组件,它可以是任何想要的尺寸,但不能超过屏幕大小。
子组件是一个绿色的 Container,想要 30 × 30 的尺寸。鉴于红色 Container 将自身尺寸调整为子组件的尺寸,它也变成了 30 × 30。红色不可见,因为绿色 Container 完全覆盖了红色 Container。
示例 8
#
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
红色 Container 将自身调整为子组件的尺寸,但考虑到了它自己的内边距。因此它是 30 × 30 加上内边距的尺寸。由于内边距的存在,红色可见,而绿色 Container 与上一个示例中的尺寸相同。
示例 9
#
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
#
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
#
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
#
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 像素,这就是它得到的尺寸,因为 100 在 70 到 150 之间。
示例 13
#
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
屏幕强制 UnconstrainedBox 与屏幕大小完全一致。然而,UnconstrainedBox 让其子组件 Container 可以是任何它想要的尺寸。
示例 14
#
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
屏幕强制 UnconstrainedBox 与屏幕大小完全一致,而 UnconstrainedBox 让其子组件 Container 可以是任何它想要的尺寸。
不幸的是,在这种情况下,Container 的宽度为 4000 像素,太大了,无法放入 UnconstrainedBox 中,因此 UnconstrainedBox 会显示令人恐惧的“溢出警告”。
示例 15
#
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
#
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
这不会渲染任何东西,并且你会在控制台中看到一个错误。
UnconstrainedBox 让其子组件成为任何它想要的尺寸,然而其子组件是一个尺寸无限的 Container。
Flutter 无法渲染无限的尺寸,所以它抛出一个错误,显示以下消息:BoxConstraints forces an infinite width.
示例 17
#
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
在这里你不会再收到错误了,因为当 LimitedBox 被 UnconstrainedBox 给定一个无限尺寸时,它会向下向其子组件传递一个 100 的最大宽度。
如果你将 UnconstrainedBox 换成 Center 组件,LimitedBox 将不再应用其限制(因为它的限制只在获得无限约束时才应用),并且 Container 的宽度可以增长到超过 100。
这解释了 LimitedBox 和 ConstrainedBox 之间的区别。
示例 18
#
const FittedBox(child: Text('Some Example Text.'))
屏幕强制 FittedBox 与屏幕大小完全一致。Text 具有一些自然的宽度(也称为其固有宽度),这取决于文本内容、字体大小等。
FittedBox 让 Text 成为任何它想要的尺寸,但在 Text 向 FittedBox 报告其尺寸后,FittedBox 会缩放文本,直到它填满所有可用宽度。
示例 19
#
const Center(child: FittedBox(child: Text('Some Example Text.')))
但是,如果你把 FittedBox 放在 Center 组件里面会发生什么?Center 让 FittedBox 成为它想要的任何尺寸,上限为屏幕大小。
然后 FittedBox 将自身大小调整为 Text,并让 Text 成为它想要的任何尺寸。由于 FittedBox 和 Text 的尺寸相同,因此不会发生缩放。
示例 20
#
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
#
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
#
FittedBox(
child: Container(height: 20, width: double.infinity, color: Colors.red),
)
FittedBox 只能缩放一个有界的组件(具有非无限的宽度和高度)。否则,它将不会渲染任何东西,并且你会在控制台中看到错误。
示例 23
#
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
#
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
#
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
#
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
#
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 允许其子组件拥有与 Flexible 本身相同或更小的宽度,而 Expanded 则强制其子组件拥有与 Expanded 完全相同的宽度。但无论是 Expanded 还是 Flexible,在调整自身大小时都会忽略其子组件的宽度。
示例 28
#
Scaffold(
body: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
)
屏幕强制 Scaffold 与屏幕大小完全一致,所以 Scaffold 填满了屏幕。Scaffold 告诉 Container 它可以是任何想要的尺寸,但不能超过屏幕大小。
示例 29
#
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
),
)
如果你想让 Scaffold 的子组件与 Scaffold 本身的尺寸完全一致,你可以用 SizedBox.expand 包裹它的子组件。
紧密 vs 松散约束
#经常听到某种约束是“紧密的 (tight)”或“松散的 (loose)”,这意味着什么?
紧密约束
#紧密约束提供了一个单一的可能性,一个确切的尺寸。换句话说,紧密约束的最大宽度等于其最小宽度,最大高度等于其最小高度。
一个例子是 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 传递紧密约束来实现这一点的。
松散约束
#松散约束是指最小值为零且最大值非零的约束。
一些盒子会放松 (loosen) 传入的约束,这意味着保持了最大值,但删除了最小值,因此组件的最小宽度和高度都可以等于零。
最终,Center 的目的是将它从父组件(屏幕)接收到的紧密约束转换为其子组件(Container)的松散约束。
如果你重温 示例 3,Center 允许红色 Container 比屏幕小,但不能比屏幕大。
无界约束
#在某些情况下,盒子的约束是无界的 (unbounded) 或无限的。这意味着最大宽度或最大高度被设置为 double.infinity。
一个尝试尽可能变大的盒子在给定无界约束时无法有效工作,并且在调试模式下会抛出异常。
渲染盒子最终出现无界约束的最常见情况是在弹性盒子(Row 或 Column)内,以及在可滚动区域内(例如 ListView 和其他 ScrollView 子类)。
例如,ListView 尝试扩展以适应其交叉方向上的可用空间(可能它是一个垂直滚动的块,并尝试像其父组件一样宽)。如果你将一个垂直滚动的 ListView 嵌套在水平滚动的 ListView 中,内部列表会尝试变得尽可能宽,即无限宽,因为外部列表在那个方向上是可滚动的。
下一节将描述你在 Flex 组件中遇到无界约束时可能遇到的错误。
Flex
#弹性盒子(Row 和 Column)的表现取决于其约束在主轴方向上是有界还是无界的。
在主轴方向上具有有界约束的弹性盒子会尝试尽可能变大。
在主轴方向上具有无界约束的弹性盒子会尝试在其空间内容纳其子组件。每个子组件的 flex 值必须设置为零,这意味着当弹性盒子在另一个弹性盒子或可滚动区域内时,你不能使用 Expanded;否则它会抛出异常。
交叉 (cross) 方向(对于 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执行布局的方法。
原文作者:Marcelo Glasberg
Marcelo 最初在 Medium 上以 《Flutter:即使是初学者也必须知道的高级布局规则》 为题发布了此内容。我们非常喜欢它,并请求他允许我们在 docs.flutter.dev 上发布,他慷慨地同意了。谢谢你,Marcelo!你可以在 GitHub 和 pub.dev 上找到 Marcelo。
此外,感谢 Simon Lightfoot 创作了文章顶部的标题图片。