理解約束
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 創作了文章頂部的標題圖片。