理解約束
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(彈性盒子),根據它們收到的約束而變化,如 彈性 部分所述。
示例
#為了獲得互動式體驗,請使用以下 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 =
'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 畫素,並且由於它在 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 強制使用無限寬度。
示例 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 包裝其子元件。
緊約束與寬鬆約束
#經常聽到某些約束是“緊約束”或“寬鬆約束”,那麼這意味著什麼?
緊約束
#緊約束提供一個單一的可能性,即確切的大小。換句話說,緊約束的其最大寬度等於其最小寬度;並且其最大高度等於其最小高度。
一個例子是 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。
當給定無邊界約束時,試圖儘可能大的框將無法正常工作,並且在除錯模式下會丟擲異常。
渲染框最終獲得無邊界約束的最常見情況是在 flex 框(Row 和 Column)內,以及在可滾動區域內(例如 ListView 和其他 ScrollView 子類)。
例如,ListView 嘗試擴充套件以適應其交叉方向中的可用空間(也許它是一個垂直滾動塊,並嘗試與它的父元件一樣寬)。如果你將垂直滾動的 ListView 巢狀在水平滾動的 ListView 中,則內部列表嘗試儘可能寬,這會無限寬,因為外部列表在該方向上是可滾動的。
下一節描述了你在 Flex 元件中遇到無邊界約束時可能遇到的錯誤。
Flex
#Flex 框(Row 和 Column)根據其主要方向上的約束是邊界還是無邊界,表現不同。
具有其主要方向上邊界約束的 flex 框嘗試儘可能大。
具有其主要方向上無邊界約束的 flex 框嘗試將其子元件適應到該空間中。每個子元件的 flex 值必須設定為零,這意味著你不能在 flex 框位於另一個 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佈局的方法。
Marcelo Glasberg 的原始文章
Marcelo 最初將此內容作為 Flutter: The Advanced Layout Rule Even Beginners Must Know 在 Medium 上釋出。我們非常喜歡它,並要求他允許我們在 docs.flutter.dev 上釋出,他欣然同意。謝謝你,Marcelo!你可以在 GitHub 和 pub.dev 上找到 Marcelo。
此外,感謝 Simon Lightfoot 建立文章頂部的頁首圖片。