Skip to main content

理解约束

文章的英雄图像

当学习 Flutter 的人问你为什么某些带有 width: 100 的小部件宽度不是 100 像素时, 默认答案是告诉他们将该小部件放在 Center 内,对吧?

不要这样做。

如果你这样做,他们会一次又一次地回来, 问为什么某些 FittedBox 不起作用, 为什么那个 Column 溢出了,或者 IntrinsicWidth 应该做什么。

相反,首先告诉他们 Flutter 布局与 HTML 布局(这可能是他们的来源)非常不同, 然后让他们记住以下规则:

约束向下传递。尺寸向上传递。父级设置位置。

如果不了解此规则,就无法真正理解 Flutter 布局,因此 Flutter 开发人员应该尽早学习它。

更详细地说:

  • 小部件从其 父级 获取自己的 约束约束 只是一组 4 个双精度数: 最小宽度和最大宽度,以及最小高度和最大高度。
  • 然后,小部件遍历其自己的 子级 列表。 小部件逐个告诉其子级它们的 约束是什么(每个子级的约束可能不同), 然后询问每个子级想要多大。
  • 然后,小部件逐个定位其子级 (水平方向在 x 轴上,垂直方向在 y 轴上)。
  • 最后,小部件告诉其父级其自身的尺寸 (当然,在原始约束范围内)。

例如,如果一个组合小部件包含一个带有某些填充的列,并且想要如下布局其两个子级:

视觉布局

协商过程如下:

小部件:“嘿,父级,我的约束是什么?”

父级:“你的宽度必须在 0300 像素之间, 高度必须在 085 之间。”

小部件:“嗯,由于我想有 5 像素的填充, 那么我的子级的宽度最多可以有 290 像素, 高度最多可以有 75 像素。”

小部件:“嘿,第一个子级,你的宽度必须在 0290 像素之间,高度必须在 075 之间。”

第一个子级:“好的,那么我希望宽度为 290 像素, 高度为 20 像素。”

小部件:“嗯,由于我想把我的第二个子级放在第一个子级下面, 这只能为我的第二个子级留下 55 像素的高度。”

小部件:“嘿,第二个子级,你的宽度必须在 0290 之间, 高度必须在 055 之间。”

第二个子级:“好的,我希望宽度为 140 像素, 高度为 30 像素。”

小部件:“很好。我的第一个子级的 x 坐标为 5y 坐标为 5, 我的第二个子级的 x 坐标为 80y 坐标为 25。”

小部件:“嘿,父级,我已经决定我的尺寸将是 300 像素宽,60 像素高。”

限制

#

Flutter 的布局引擎设计为单遍过程。 这意味着 Flutter 布局其小部件非常高效, 但这确实导致了一些限制:

  • 小部件只能在其父级给定的约束范围内决定其自身的大小。 这意味着小部件通常 不能具有任何想要的大小

  • 小部件 无法知道也无法决定其在屏幕上的位置 , 因为是小部件的父级决定小部件的位置。

  • 由于父级的大小和位置又取决于其自身的父级, 因此如果不考虑整个树,就无法精确地定义任何小部件的大小和位置。

  • 如果子级想要的大小与其父级不同,并且 父级没有足够的信息来对其进行对齐, 那么子级的大小可能会被忽略。 定义对齐时要具体。

在 Flutter 中,小部件由其底层的 RenderBox 对象呈现。Flutter 中的许多框, 尤其是那些只接受一个子级的框, 会将其约束传递给其子级。

一般来说,就它们如何处理其约束而言,有三种类型的框:

  • 那些试图尽可能大的框。 例如,CenterListView 使用的框。
  • 那些试图与其子级大小相同的框。 例如,TransformOpacity 使用的框。
  • 那些试图达到特定大小的框。 例如,ImageText 使用的框。

某些小部件,例如 Container, 根据其构造函数参数而异。Container 构造函数默认 尝试尽可能大,但如果您为其指定 width, 例如,它会尝试遵守该值并达到该特定大小。

其他小部件,例如 RowColumn(弹性框) 根据它们获得的约束而异, 如“Flex”部分所述。

示例

#

为了获得交互式体验,请使用以下 DartPad。 使用编号的水平滚动条在 29 个不同的示例之间切换。

import 'package:flutter/material.dart';

void main() => runApp(const HomePage());

const red = Colors.red;
const green = Colors.green;
const blue = Colors.blue;
const big = TextStyle(fontSize: 30);

//////////////////////////////////////////////////

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return const FlutterLayoutArticle([
      Example1(),
      Example2(),
      Example3(),
      Example4(),
      Example5(),
      Example6(),
      Example7(),
      Example8(),
      Example9(),
      Example10(),
      Example11(),
      Example12(),
      Example13(),
      Example14(),
      Example15(),
      Example16(),
      Example17(),
      Example18(),
      Example19(),
      Example20(),
      Example21(),
      Example22(),
      Example23(),
      Example24(),
      Example25(),
      Example26(),
      Example27(),
      Example28(),
      Example29(),
    ]);
  }
}

//////////////////////////////////////////////////

abstract class Example extends StatelessWidget {
  const Example({super.key});

  String get code;

  String get explanation;
}

//////////////////////////////////////////////////

class FlutterLayoutArticle extends StatefulWidget {
  const FlutterLayoutArticle(
    this.examples, {
    super.key,
  });

  final List<Example> examples;

  @override
  State<FlutterLayoutArticle> createState() => _FlutterLayoutArticleState();
}

//////////////////////////////////////////////////

class _FlutterLayoutArticleState extends State<FlutterLayoutArticle> {
  late int count;
  late Widget example;
  late String code;
  late String explanation;

  @override
  void initState() {
    count = 1;
    code = const Example1().code;
    explanation = const Example1().explanation;

    super.initState();
  }

  @override
  void didUpdateWidget(FlutterLayoutArticle oldWidget) {
    super.didUpdateWidget(oldWidget);
    var example = widget.examples[count - 1];
    code = example.code;
    explanation = example.explanation;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Layout Article',
      home: SafeArea(
        child: Material(
          color: Colors.black,
          child: FittedBox(
            child: Container(
              width: 400,
              height: 670,
              color: const Color(0xFFCCCCCC),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  Expanded(
                      child: ConstrainedBox(
                          constraints: const BoxConstraints.tightFor(
                              width: double.infinity, height: double.infinity),
                          child: widget.examples[count - 1])),
                  Container(
                    height: 50,
                    width: double.infinity,
                    color: Colors.black,
                    child: SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          for (int i = 0; i < widget.examples.length; i++)
                            Container(
                              width: 58,
                              padding: const EdgeInsets.only(left: 4, right: 4),
                              child: button(i + 1),
                            ),
                        ],
                      ),
                    ),
                  ),
                  Container(
                    height: 273,
                    color: Colors.grey[50],
                    child: Scrollbar(
                      child: SingleChildScrollView(
                        key: ValueKey(count),
                        child: Padding(
                          padding: const EdgeInsets.all(10),
                          child: Column(
                            children: [
                              Center(child: Text(code)),
                              const SizedBox(height: 15),
                              Text(
                                explanation,
                                style: TextStyle(
                                    color: Colors.blue[900],
                                    fontStyle: FontStyle.italic),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget button(int exampleNumber) {
    return Button(
      key: ValueKey('button$exampleNumber'),
      isSelected: count == exampleNumber,
      exampleNumber: exampleNumber,
      onPressed: () {
        showExample(
          exampleNumber,
          widget.examples[exampleNumber - 1].code,
          widget.examples[exampleNumber - 1].explanation,
        );
      },
    );
  }

  void showExample(int exampleNumber, String code, String explanation) {
    setState(() {
      count = exampleNumber;
      this.code = code;
      this.explanation = explanation;
    });
  }
}

//////////////////////////////////////////////////

class Button extends StatelessWidget {
  final bool isSelected;
  final int exampleNumber;
  final VoidCallback onPressed;

  const Button({
    super.key,
    required this.isSelected,
    required this.exampleNumber,
    required this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return TextButton(
      style: TextButton.styleFrom(
        foregroundColor: Colors.white,
        backgroundColor: isSelected ? Colors.grey : Colors.grey[800],
      ),
      child: Text(exampleNumber.toString()),
      onPressed: () {
        Scrollable.ensureVisible(
          context,
          duration: const Duration(milliseconds: 350),
          curve: Curves.easeOut,
          alignment: 0.5,
        );
        onPressed();
      },
    );
  }
}
//////////////////////////////////////////////////

class Example1 extends Example {
  const Example1({super.key});

  @override
  final code = 'Container(color: red)';

  @override
  final explanation = '屏幕是 Container 的父级,'
      '它强制 Container 的大小与屏幕完全相同。'
      '\n\n'
      '因此,Container 填充屏幕并将其涂成红色。';

  @override
  Widget build(BuildContext context) {
    return Container(color: red);
  }
}

//////////////////////////////////////////////////

class Example2 extends Example {
  const Example2({super.key});

  @override
  final code = 'Container(width: 100, height: 100, color: red)';
  @override
  final String explanation =
      '红色的 Container 想要成为 100x100,但它做不到,'
      '因为屏幕强制它的大小与屏幕完全相同。'
      '\n\n'
      '所以 Container 填充屏幕。';

  @override
  Widget build(BuildContext context) {
    return Container(width: 100, height: 100, color: red);
  }
}

//////////////////////////////////////////////////

class Example3 extends Example {
  const Example3({super.key});

  @override
  final code = 'Center(\n'
      '   child: Container(width: 100, height: 100, color: red))';
  @override
  final String explanation =
      '屏幕强制 Center 的大小与屏幕完全相同,'
      '因此 Center 填充屏幕。'
      '\n\n'
      'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
      '现在 Container 确实可以是 100x100。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(width: 100, height: 100, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example4 extends Example {
  const Example4({super.key});

  @override
  final code = 'Align(\n'
      '   alignment: Alignment.bottomRight,\n'
      '   child: Container(width: 100, height: 100, color: red))';
  @override
  final String explanation =
      '这与之前的示例不同,因为它使用 Align 而不是 Center。'
      '\n\n'
      'Align 也告诉 Container 它可以是任何大小,但如果有空隙,它不会将 Container 居中。'
      '相反,它将 Container 对齐到可用空间的右下角。';

  @override
  Widget build(BuildContext context) {
    return Align(
      alignment: Alignment.bottomRight,
      child: Container(width: 100, height: 100, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example5 extends Example {
  const Example5({super.key});

  @override
  final code = 'Center(\n'
      '   child: Container(\n'
      '              color: red,\n'
      '              width: double.infinity,\n'
      '              height: double.infinity))';
  @override
  final String explanation =
      '屏幕强制 Center 的大小与屏幕完全相同,'
      '因此 Center 填充屏幕。'
      '\n\n'
      'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
      'Container 想要无限大,但由于它不能大于屏幕,因此它只填充屏幕。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          width: double.infinity, height: double.infinity, color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example6 extends Example {
  const Example6({super.key});

  @override
  final code = 'Center(child: Container(color: red))';
  @override
  final String explanation =
      '屏幕强制 Center 的大小与屏幕完全相同,'
      '因此 Center 填充屏幕。'
      '\n\n'
      'Center 告诉 Container 它可以是任何大小,但不能大于屏幕。'
      '\n\n'
      '由于 Container 没有子级且没有固定大小,它决定它想要尽可能大,因此它填充整个屏幕。'
      '\n\n'
      '但是为什么 Container 会这样决定呢?'
      '仅仅是因为创建 Container 小部件的人的设计决定。'
      '它本来可以被不同地创建,您必须阅读 Container 文档才能理解它在不同情况下如何表现。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(color: red),
    );
  }
}

//////////////////////////////////////////////////

class Example7 extends Example {
  const Example7({super.key});

  @override
  final code = 'Center(\n'
      '   child: Container(color: red\n'
      '      child: Container(color: green, width: 30, height: 30)))';
  @override
  final String explanation =
      '屏幕强制 Center 的大小与屏幕完全相同,'
      '因此 Center 填充屏幕。'
      '\n\n'
      'Center 告诉红色 Container 它可以是任何大小,但不能大于屏幕。'
      '由于红色 Container 没有大小但有子级,它决定它想要与其子级大小相同。'
      '\n\n'
      '红色 Container 告诉其子级它可以是任何大小,但不能大于屏幕。'
      '\n\n'
      '子级是一个想要成为 30x30 的绿色 Container。'
      '\n\n'
      '由于红色 `Container` 没有大小但有子级,它决定它想要与其子级大小相同。'
      '红色不可见,因为绿色 Container 完全覆盖了红色 Container。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: red,
        child: Container(color: green, width: 30, height: 30),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example8 extends Example {
  const Example8({super.key});

  @override
  final code = 'Center(\n'
      '   child: Container(color: red\n'
      '      padding: const EdgeInsets.all(20),\n'
      '      child: Container(color: green, width: 30, height: 30)))';
  @override
  final String explanation =
      '红色 Container 的大小与其子级大小相同,但它考虑了自己的填充。'
      '所以它也是 30x30 加上填充。'
      '红色可见是因为填充,绿色 Container 的大小与之前的示例相同。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: const EdgeInsets.all(20),
        color: red,
        child: Container(color: green, width: 30, height: 30),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example9 extends Example {
  const Example9({super.key});

  @override
  final code = 'ConstrainedBox(\n'
      '   constraints: BoxConstraints(\n'
      '              minWidth: 70, minHeight: 70,\n'
      '              maxWidth: 150, maxHeight: 150),\n'
      '      child: Container(color: red, width: 10, height: 10)))';
  @override
  final String explanation =
      '您可能会猜想 Container 的大小必须在 70 到 150 像素之间,但您错了。'
      'ConstrainedBox 只会从其父级接收到的约束中施加附加约束。'
      '\n\n'
      '在这里,屏幕强制 ConstrainedBox 的大小与屏幕完全相同,'
      '因此它告诉其子级 Container 也假设屏幕的大小,'
      '从而忽略其“constraints”参数。';

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: const BoxConstraints(
        minWidth: 70,
        minHeight: 70,
        maxWidth: 150,
        maxHeight: 150,
      ),
      child: Container(color: red, width: 10, height: 10),
    );
  }
}

//////////////////////////////////////////////////

class Example10 extends Example {
  const Example10({super.key});

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 10, height: 10))))';
  @override
  final String explanation =
      '现在,Center 允许 ConstrainedBox 的大小达到屏幕大小。'
      '\n\n'
      'ConstrainedBox 从其“constraints”参数向其子级施加附加约束。'
      '\n\n'
      'Container 的大小必须在 70 到 150 像素之间。它想要 10 像素,所以它最终会得到 70(最小值)。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 10, height: 10),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example11 extends Example {
  const Example11({super.key});

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 1000, height: 1000))))';
  @override
  final String explanation =
      'Center 允许 ConstrainedBox 的大小达到屏幕大小。'
      'ConstrainedBox 从其“constraints”参数向其子级施加附加约束'
      '\n\n'
      'Container 的大小必须在 70 到 150 像素之间。它想要 1000 像素,所以它最终会得到 150(最大值)。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 1000, height: 1000),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example12 extends Example {
  const Example12({super.key});

  @override
  final code = 'Center(\n'
      '   child: ConstrainedBox(\n'
      '      constraints: BoxConstraints(\n'
      '                 minWidth: 70, minHeight: 70,\n'
      '                 maxWidth: 150, maxHeight: 150),\n'
      '        child: Container(color: red, width: 100, height: 100))))';
  @override
  final String explanation =
      'Center 允许 ConstrainedBox 的大小达到屏幕大小。'
      'ConstrainedBox 从其“constraints”参数向其子级施加附加约束。'
      '\n\n'
      'Container 的大小必须在 70 到 150 像素之间。它想要 100 像素,这就是它的大小,因为这在 70 和 150 之间。';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(
          minWidth: 70,
          minHeight: 70,
          maxWidth: 150,
          maxHeight: 150,
        ),
        child: Container(color: red, width: 100, height: 100),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example13 extends Example {
  const Example13({super.key});

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: red, width: 20, height: 50));';
  @override
  final String explanation =
      '屏幕强制 UnconstrainedBox 的大小与屏幕完全相同。'
      '但是,UnconstrainedBox 允许其子级 Container 的大小为任意大小。';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: red, width: 20, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example14 extends Example {
  const Example14({super.key});

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: red, width: 4000, height: 50));';
  @override
  final String explanation =
      '屏幕强制 UnconstrainedBox 的大小与屏幕完全相同,'
      '而 UnconstrainedBox 允许其子级 Container 的大小为任意大小。'
      '\n\n'
      '不幸的是,在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 UnconstrainedBox 中,'
      '因此 UnconstrainedBox 显示了令人讨厌的“溢出警告”。';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: red, width: 4000, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example15 extends Example {
  const Example15({super.key});

  @override
  final code = 'OverflowBox(\n'
      '   minWidth: 0,'
      '   minHeight: 0,'
      '   maxWidth: double.infinity,'
      '   maxHeight: double.infinity,'
      '   child: Container(color: red, width: 4000, height: 50));';
  @override
  final String explanation =
      '屏幕强制 OverflowBox 的大小与屏幕完全相同,'
      '而 OverflowBox 允许其子级 Container 的大小为任意大小。'
      '\n\n'
      'OverflowBox 类似于 UnconstrainedBox,区别在于如果子级不适合空间,它不会显示任何警告。'
      '\n\n'
      '在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 OverflowBox 中,'
      '但 OverflowBox 只显示它所能显示的内容,没有任何警告。';

  @override
  Widget build(BuildContext context) {
    return OverflowBox(
      minWidth: 0,
      minHeight: 0,
      maxWidth: double.infinity,
      maxHeight: double.infinity,
      child: Container(color: red, width: 4000, height: 50),
    );
  }
}

//////////////////////////////////////////////////

class Example16 extends Example {
  const Example16({super.key});

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: Container(color: Colors.red, width: double.infinity, height: 100));';
  @override
  final String explanation =
      '这不会渲染任何内容,您将在控制台中看到错误。'
      '\n\n'
      'UnconstrainedBox 允许其子级的大小为任意大小,'
      '但是其子级是具有无限大小的 Container。'
      '\n\n'
      'Flutter 无法渲染无限大小,因此它会抛出一个错误,错误消息如下:'
      '"BoxConstraints 强制无限宽度。"';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: Container(color: Colors.red, width: double.infinity, height: 100),
    );
  }
}

//////////////////////////////////////////////////

class Example17 extends Example {
  const Example17({super.key});

  @override
  final code = 'UnconstrainedBox(\n'
      '   child: LimitedBox(maxWidth: 100,\n'
      '      child: Container(color: Colors.red,\n'
      '                       width: double.infinity, height: 100));';
  @override
  final String explanation = '在这里,您将不再收到错误,'
      '因为当 UnconstrainedBox 为 LimitedBox 提供无限大小时,'
      '它会向下传递 100 的最大宽度给其子级。'
      '\n\n'
      '如果您将 UnconstrainedBox 与 Center 小部件互换,'
      'LimitedBox 将不再应用其限制(因为其限制仅在其获得无限约束时应用),'
      '并且 Container 的宽度允许超过 100。'
      '\n\n'
      '这解释了 LimitedBox 和 ConstrainedBox 之间的区别。';

  @override
  Widget build(BuildContext context) {
    return UnconstrainedBox(
      child: LimitedBox(
        maxWidth: 100,
        child: Container(
          color: Colors.red,
          width: double.infinity,
          height: 100,
        ),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example18 extends Example {
  const Example18({super.key});

  @override
  final code = 'FittedBox(\n'
      '   child: Text(\'Some Example Text.\'));';
  @override
  final String explanation =
      '屏幕强制 FittedBox 的大小与屏幕完全相同。'
      'Text 有一些自然宽度(也称为其内在宽度),这取决于文本量、字体大小等。'
      '\n\n'
      'FittedBox 允许 Text 的大小为任意大小,'
      '但在 Text 将其大小告诉 FittedBox 后,'
      'FittedBox 会缩放 Text,直到它填满所有可用宽度。';

  @override
  Widget build(BuildContext context) {
    return const FittedBox(
      child: Text('Some Example Text.'),
    );
  }
}

//////////////////////////////////////////////////

class Example19 extends Example {
  const Example19({super.key});

  @override
  final code = 'Center(\n'
      '   child: FittedBox(\n'
      '      child: Text(\'Some Example Text.\')));';
  @override
  final String explanation =
      '但是,如果您将 FittedBox 放入 Center 小部件中会发生什么?'
      'Center 允许 FittedBox 的大小达到屏幕大小。'
      '\n\n'
      '然后,FittedBox 将自身大小调整为 Text,并允许 Text 的大小为任意大小。'
      '\n\n'
      '由于 FittedBox 和 Text 的大小相同,因此不会发生缩放。';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: FittedBox(
        child: Text('Some Example Text.'),
      ),
    );
  }
}

////////////////////////////////////////////////////

class Example20 extends Example {
  const Example20({super.key});

  @override
  final code = 'Center(\n'
      '   child: FittedBox(\n'
      '      child: Text(\'…\')));';
  @override
  final String explanation =
      '但是,如果 FittedBox 在 Center 小部件内,但 Text 太大而无法适应屏幕会发生什么?'
      '\n\n'
      'FittedBox 试图将其自身大小调整为 Text,但它不能大于屏幕。'
      '然后它假设屏幕大小,并调整 Text 的大小,使其也适合屏幕。';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: FittedBox(
        child: Text(
            'This is some very very very large text that is too big to fit a regular screen in a single line.'),
      ),
    );
  }
}

//////////////////////////////////////////////////

class Example21 extends Example {
  const Example21({super.key});

  @override
  final code = 'Center(\n'
      '   child: Text(\'…\'));';
  @override
  final String explanation = '但是,如果您移除 FittedBox,'
      'Text 从屏幕获取其最大宽度,'
      '并换行以适应屏幕。';

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text(
          'This is some very very very large text that is too big to fit a regular screen in a single line.'),
    );
  }
}

//////////////////////////////////////////////////

class Example22 extends Example {
  const Example22({super.key});

  @override
  final code = 'FittedBox(\n'
      '   如果您愿意,可以从
[这个 GitHub 仓库](https://github.com/marcglasberg/flutter_layout_article) 获取代码。

以下章节解释了这些示例。

[这个 GitHub 仓库]: https://github.com/marcglasberg/flutter_layout_article

### 示例 1

<img src='/assets/images/docs/ui/layout/layout-1.png' class="mw-100" alt="示例 1 布局">

<?code-excerpt "lib/main.dart (Example1)" replace="/(return |;)//g"?>
```dart
Container(color: red)

屏幕是 Container 的父级,它强制 Container 的大小与屏幕完全相同。

因此,Container 填充屏幕并将其涂成红色。

示例 2

#
示例 2 布局
dart
Container(width: 100, height: 100, color: red)

红色的 Container 想要成为 100 × 100, 但它做不到,因为屏幕强制它的大小与屏幕完全相同。

因此,Container 填充屏幕。

示例 3

#
示例 3 布局
dart
Center(
  child: Container(width: 100, height: 100, color: red),
)

屏幕强制 Center 的大小与屏幕完全相同,因此 Center 填充屏幕。

Center 告诉 Container 它可以是任何大小,但不能大于屏幕。现在 Container 确实可以是 100 × 100。

示例 4

#
示例 4 布局
dart
Align(
  alignment: Alignment.bottomRight,
  child: Container(width: 100, height: 100, color: red),
)

这与之前的示例不同,因为它使用 Align 而不是 Center

Align 也告诉 Container 它可以是任何大小,但如果有空隙,它不会将 Container 居中。 相反,它将容器对齐到可用空间的右下角。

示例 5

#
示例 5 布局
dart
Center(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

屏幕强制 Center 的大小与屏幕完全相同,因此 Center 填充屏幕。

Center 告诉 Container 它可以是任何大小,但不能大于屏幕。Container 想要无限大,但由于它不能大于屏幕,因此它只填充屏幕。

示例 6

#
示例 6 布局
dart
Center(
  child: Container(color: red),
)

屏幕强制 Center 的大小与屏幕完全相同,因此 Center 填充屏幕。

Center 告诉 Container 它可以是任何大小,但不能大于屏幕。 由于 Container 没有子级且没有固定大小, 它决定它想要尽可能大, 因此它填充整个屏幕。

但是为什么 Container 会这样决定呢? 仅仅是因为创建 Container 小部件的人的设计决定。它本来可以被不同地创建,您必须阅读 Container 的 API 文档才能理解它在不同情况下如何表现。

示例 7

#
示例 7 布局
dart
Center(
  child: Container(
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

屏幕强制 Center 的大小与屏幕完全相同,因此 Center 填充屏幕。

Center 告诉红色的 Container 它可以是任何大小,但不能大于屏幕。由于红色的 Container 没有大小但有子级,它决定它想要与其子级大小相同。

红色的 Container 告诉其子级它可以是任何大小,但不能大于屏幕。

子级是一个想要成为 30 × 30 的绿色 Container。鉴于红色的 Container 将自身大小调整为其子级的大小,它也是 30 × 30。 红色不可见,因为绿色 Container 完全覆盖了红色的 Container

示例 8

#
示例 8 布局
dart
Center(
  child: Container(
    padding: const EdgeInsets.all(20),
    color: red,
    child: Container(color: green, width: 30, height: 30),
  ),
)

红色的 Container 将自身大小调整为其子级的大小, 但它考虑了自己的填充。 所以它也是 30 × 30 加上填充。 红色可见是因为填充, 而绿色的 Container 与之前的示例大小相同。

示例 9

#
示例 9 布局
dart
ConstrainedBox(
  constraints: const BoxConstraints(
    minWidth: 70,
    minHeight: 70,
    maxWidth: 150,
    maxHeight: 150,
  ),
  child: Container(color: red, width: 10, height: 10),
)

您可能会猜想 Container 的大小必须在 70 到 150 像素之间,但您错了。 ConstrainedBox 只会从其父级接收到的约束中施加 额外 的约束。

在这里,屏幕强制 ConstrainedBox 的大小与屏幕完全相同,因此它告诉其子级 Container 也假设屏幕的大小,从而忽略其 constraints 参数。

示例 10

#
示例 10 布局
dart
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 10, height: 10),
  ),
)

现在,Center 允许 ConstrainedBox 的大小达到屏幕大小。ConstrainedBox 从其 constraints 参数向其子级施加 额外 的约束。

Container 的大小必须在 70 到 150 像素之间。它想要 10 像素,所以它最终会得到 70(最小值)。

示例 11

#
示例 11 布局
dart
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 1000, height: 1000),
  ),
)

Center 允许 ConstrainedBox 的大小达到屏幕大小。ConstrainedBox 从其 constraints 参数向其子级施加 额外 的约束。

Container 的大小必须在 70 到 150 像素之间。它想要 1000 像素,所以它最终会得到 150(最大值)。

示例 12

#
示例 12 布局
dart
Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      minWidth: 70,
      minHeight: 70,
      maxWidth: 150,
      maxHeight: 150,
    ),
    child: Container(color: red, width: 100, height: 100),
  ),
)

Center 允许 ConstrainedBox 的大小达到屏幕大小。ConstrainedBox 从其 constraints 参数向其子级施加 额外 的约束。

Container 的大小必须在 70 到 150 像素之间。它想要 100 像素,这就是它的大小,因为这在 70 和 150 之间。

示例 13

#
示例 13 布局
dart
UnconstrainedBox(
  child: Container(color: red, width: 20, height: 50),
)

屏幕强制 UnconstrainedBox 的大小与屏幕完全相同。但是,UnconstrainedBox 允许其子级 Container 的大小为任意大小。

示例 14

#
示例 14 布局
dart
UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

屏幕强制 UnconstrainedBox 的大小与屏幕完全相同,而 UnconstrainedBox 允许其子级 Container 的大小为任意大小。

不幸的是,在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 UnconstrainedBox 中,因此 UnconstrainedBox 显示了令人讨厌的“溢出警告”。

示例 15

#
示例 15 布局
dart
OverflowBox(
  minWidth: 0,
  minHeight: 0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
  child: Container(color: red, width: 4000, height: 50),
)

屏幕强制 OverflowBox 的大小与屏幕完全相同,而 OverflowBox 允许其子级 Container 的大小为任意大小。

OverflowBox 类似于 UnconstrainedBox; 区别在于,如果子级不适合空间,它不会显示任何警告。

在这种情况下,Container 的宽度为 4000 像素,并且太大而无法放入 OverflowBox 中, 但 OverflowBox 只显示它所能显示的内容,没有任何警告。

示例 16

#
示例 16 布局
dart
UnconstrainedBox(
  child: Container(color: Colors.red, width: double.infinity, height: 100),
)

这不会渲染任何内容,您将在控制台中看到错误。

UnconstrainedBox 允许其子级的大小为任意大小, 但是其子级是具有无限大小的 Container

Flutter 无法渲染无限大小,因此它会抛出一个错误,错误消息如下:BoxConstraints forces an infinite width.

示例 17

#
示例 17 布局
dart
UnconstrainedBox(
  child: LimitedBox(
    maxWidth: 100,
    child: Container(
      color: Colors.red,
      width: double.infinity,
      height: 100,
    ),
  ),
)

在这里,您将不再收到错误, 因为当 UnconstrainedBoxLimitedBox 提供无限大小时, 它会向下传递 100 的最大宽度给其子级。

如果您将 UnconstrainedBoxCenter 小部件互换, LimitedBox 将不再应用其限制(因为其限制仅在其获得无限约束时应用),并且 Container 的宽度允许超过 100。

这解释了 LimitedBoxConstrainedBox 之间的区别。

示例 18

#
示例 18 布局
dart
const FittedBox(
  child: Text('Some Example Text.'),
)

屏幕强制 FittedBox 的大小与屏幕完全相同。Text 有一些自然宽度(也称为其内在宽度),这取决于 文本量、字体大小等。

FittedBox 允许 Text 的大小为任意大小, 但在 Text 将其大小告诉 FittedBox 后, FittedBox 会缩放 Text,直到它填满所有可用宽度。

示例 19

#
示例 19 布局
dart
const Center(
  child: FittedBox(
    child: Text('Some Example Text.'),
  ),
)

但是,如果您将 FittedBox 放入 Center 小部件中会发生什么?Center 允许 FittedBox 的大小达到屏幕大小。

FittedBox 然后将自身大小调整为 Text,并允许 Text 的大小为任意大小。 由于 FittedBoxText 的大小相同,因此不会发生缩放。

示例 20

#
示例 20 布局
dart
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.'),
  ),
)

但是,如果 FittedBoxCenter 小部件内,但 Text 太大而无法适应屏幕会发生什么?

FittedBox 试图将其自身大小调整为 Text, 但它不能大于屏幕。 然后它假设屏幕大小, 并调整 Text 的大小,使其也适合屏幕。

示例 21

#
示例 21 布局
dart
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.'),
)

但是,如果您移除 FittedBoxText 将从屏幕获取其最大宽度,并换行以适应屏幕。

示例 22

#
示例 22 布局
dart
FittedBox(
  child: Container(
    height: 20,
    width: double.infinity,
    color: Colors.red,
  ),
)

FittedBox 只能缩放边界内的小部件(宽度和高度非无限)。否则,它不会渲染任何内容,您将在控制台中看到错误。

示例 23

#
示例 23 布局
dart
Row(
  children: [
    Container(color: red, child: const Text('Hello!', style: big)),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

屏幕强制 Row 的大小与屏幕完全相同。

就像 UnconstrainedBox 一样,Row 不会对其子级施加任何约束,而是允许它们的大小为任意大小。 然后,Row 将它们并排放置,任何额外的空间都保持为空。

示例 24

#
示例 24 布局
dart
Row(
  children: [
    Container(
      color: red,
      child: const Text(
        'This is a very long text that '
        'won\'t fit the line.',
        style: big,
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

由于 Row 不会对其子级施加任何约束,因此子级可能太大而无法适应 Row 的可用宽度。在这种情况下,就像 UnconstrainedBox 一样,Row 会显示“溢出警告”。

示例 25

#
示例 25 布局
dart
Row(
  children: [
    Expanded(
      child: Center(
        child: Container(
          color: red,
          child: const Text(
            'This is a very long text that won\'t fit the line.',
            style: big,
          ),
        ),
      ),
    ),
    Container(color: green, child: const Text('Goodbye!', style: big)),
  ],
)

Row 的子级被 Expanded 小部件包裹时,Row 将不再允许此子级定义其自身宽度。

相反,它会根据其他子级定义 Expanded 的宽度,然后 Expanded 小部件才会强制原始子级具有 Expanded 的宽度。

换句话说,一旦使用 Expanded,原始子级的宽度就变得无关紧要,并且会被忽略。

示例 26

#
示例 26 布局
dart
Row(
  children: [
    Expanded(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Expanded(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

如果所有 Row 的子级都被 Expanded 小部件包裹,则每个 Expanded 的大小与其 flex 参数成比例,然后每个 Expanded 小部件才会强制其子级具有 Expanded 的宽度。

换句话说,Expanded 会忽略其子级的首选宽度。

示例 27

#
示例 27 布局
dart
Row(
  children: [
    Flexible(
      child: Container(
        color: red,
        child: const Text(
          'This is a very long text that won\'t fit the line.',
          style: big,
        ),
      ),
    ),
    Flexible(
      child: Container(
        color: green,
        child: const Text(
          'Goodbye!',
          style: big,
        ),
      ),
    ),
  ],
)

如果您使用 Flexible 代替 Expanded,唯一的区别是 Flexible 允许其子级具有与其自身相同或更小的宽度,而 Expanded 会强制其子级具有与 Expanded 完全相同的宽度。 但 ExpandedFlexible 在确定自身大小时都会忽略其子级的宽度。

示例 28

#
示例 28 布局
dart
Scaffold(
  body: Container(
    color: blue,
    child: const Column(
      children: [
        Text('Hello!'),
        Text('Goodbye!'),
      ],
    ),
  ),
)

屏幕强制 Scaffold 的大小与屏幕完全相同,因此 Scaffold 填充屏幕。 Scaffold 告诉 Container 它可以是任何大小,但不能大于屏幕。

示例 29

#
示例 29 布局
dart
Scaffold(
  body: SizedBox.expand(
    child: Container(
      color: blue,
      child: const Column(
        children: [
          Text('Hello!'),
          Text('Goodbye!'),
        ],
      ),
    ),
  ),
)

如果您希望 Scaffold 的子级大小与其自身完全相同,则可以使用 SizedBox.expand 包裹其子级。

紧凑型约束与宽松型约束

#

经常会听到某些约束是“紧凑型”或“宽松型”,那么这意味着什么呢?

紧凑型约束

#

紧凑型 约束只提供一种可能性,即精确的大小。换句话说,紧凑型约束的最大宽度等于其最小宽度;最大高度等于其最小高度。

一个例子是 App 小部件,它由 RenderView 类包含:应用程序的 build 函数返回的子级使用的框会收到一个约束,该约束强制它完全填充应用程序的内容区域(通常是整个屏幕)。

另一个例子:如果您在应用程序渲染树的根部嵌套一堆框,它们都将完全彼此契合,这是由框的紧凑型约束强制执行的。

如果您转到 Flutter 的 box.dart 文件并搜索 BoxConstraints 构造函数,您将找到以下内容:

dart
BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

如果您重新访问示例 2,屏幕会强制红色的 Container 的大小与屏幕完全相同。当然,屏幕是通过向 Container 传递紧凑型约束来实现这一点的。

宽松型约束

#

宽松型 约束是指最小值为零而最大值非零的约束。

某些框会 放松 传入的约束,这意味着最大值保持不变,但最小值被移除,因此小部件可以具有 最小 宽度和高度都等于

最终,Center 的目的是将其从父级(屏幕)接收到的紧凑型约束转换为其子级(Container)的宽松型约束。

如果您重新访问示例 3Center 允许红色的 Container 更小,但不允许大于屏幕。

无界约束

#

在某些情况下,框的约束是_无界的_ 或无限的。这意味着最大宽度或最大高度被设置为 double.infinity

当给出无界约束时,试图尽可能大的框将无法正常工作,并且在调试模式下会抛出异常。

渲染框最终具有无界约束的最常见情况是在弹性框(RowColumn)内,以及在 可滚动区域内 (例如 ListView 和其他 ScrollView 子类)。

例如,ListView 会尝试扩展以适应其交叉方向中可用的空间(也许它是一个垂直滚动的块,并试图与其父级一样宽)。如果您将垂直滚动的 ListView 嵌套在水平滚动的 ListView 内,则内部列表将尝试尽可能宽,这将是无限宽的,因为外部列表在此方向上是可滚动的。

下一节描述了您可能在 Flex 小部件中遇到的无界约束错误。

Flex

#

弹性框(RowColumn)的行为不同,这取决于其约束在其主方向上是有界的还是无界的。

主方向上具有界限约束的弹性框将

主方向上试图尽可能大。

主方向上具有无界约束的弹性框试图在其空间中容纳其子级。每个子级的 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:即使是初学者也必须知道的进阶布局规则 在 Medium 上。我们非常喜欢它,并请求他允许我们在 docs.flutter.dev 上发布,他欣然同意了。谢谢,Marcelo!您可以在 GitHubpub.dev 上找到 Marcelo。

此外,感谢 Simon Lightfoot 创建文章顶部的标题图像。