跳至主要内容

构建 Flutter 布局

本教程解释了如何在 Flutter 中设计和构建布局。

如果你使用提供的示例代码,你可以构建以下应用。

The finished app.
完成后的应用。

照片由 Dino ReichmuthUnsplash 上拍摄。文字由 瑞士旅游局 提供。

要更好地了解布局机制,请从 Flutter 的布局方法 开始。

绘制布局图

#

在本节中,考虑一下你希望为你的应用用户提供哪种类型的用户体验。

考虑如何定位用户界面组件。布局包含这些定位的最终结果。考虑规划你的布局以加快你的编码速度。使用视觉提示来了解屏幕上某个东西的位置可以提供很大的帮助。

使用你喜欢的任何方法,例如界面设计工具或铅笔和一张纸。在编写代码之前,先确定要将元素放置在屏幕上的哪个位置。这是编程版的格言:“量两次,切一次”。

  1. 提出以下问题,将布局分解为其基本元素。

    • 你能识别出行和列吗?
    • 布局是否包含网格?
    • 是否有重叠的元素?
    • UI 是否需要标签?
    • 你需要对齐、填充或边框哪些内容?
  2. 识别较大的元素。在本例中,你将图像、标题、按钮和描述排列成一列。

    Major elements in the layout: image, row, row, and text block
    布局中的主要元素:图像、行、行和文本块
  3. 绘制每个行的示意图。

    1. 第 1 行,即**标题**部分,有三个子元素:一个文本列、一个星形图标和一个数字。它的第一个子元素,即列,包含两行文本。该第一列可能需要更多空间。

      Title section with text blocks and an icon
      包含文本块和图标的标题部分
    2. 第 2 行,即**按钮**部分,有三个子元素:每个子元素包含一个列,然后包含一个图标和文本。

      The Button section with three labeled buttons
      包含三个带标签按钮的按钮部分

绘制布局示意图后,考虑如何对其进行编码。

你会将所有代码都写入一个类中吗?或者,你会为布局的每个部分创建一个类吗?

为了遵循 Flutter 的最佳实践,创建一个类或小部件来包含布局的每个部分。当 Flutter 需要重新渲染 UI 的一部分时,它会更新发生变化的最小部分。这就是为什么 Flutter 将“一切视为小部件”。如果 Text 小部件中的只有文本发生更改,Flutter 只会重新绘制该文本。Flutter 对用户输入的响应尽可能少地更改 UI。

在本教程中,将你识别出的每个元素都编写成其自身的小部件。

创建应用基础代码

#

在本节中,将基本 Flutter 应用代码提取出来以启动你的应用。

  1. 设置你的 Flutter 环境.

  2. 创建一个新的 Flutter 应用.

  3. lib/main.dart 的内容替换为以下代码。此应用使用一个参数作为应用标题和应用 appBar 上显示的标题。此决定简化了代码。

    dart
    import 'package:flutter/material.dart';
    
    void main() => runApp(const MyApp());
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        const String appTitle = 'Flutter layout demo';
        return MaterialApp(
          title: appTitle,
          home: Scaffold(
            appBar: AppBar(
              title: const Text(appTitle),
            ),
            body: const Center(
              child: Text('Hello World'),
            ),
          ),
        );
      }
    }

添加标题部分

#

在本节中,创建一个 TitleSection 小部件,使其类似于以下布局。

The Title section as sketch and prototype UI
标题部分作为草图和原型 UI

添加 TitleSection 小部件

#

MyApp 类之后添加以下代码。

dart
class TitleSection extends StatelessWidget {
  const TitleSection({
    super.key,
    required this.name,
    required this.location,
  });

  final String name;
  final String location;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Row(
        children: [
          Expanded(
            /*1*/
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                /*2*/
                Padding(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: Text(
                    name,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  location,
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          /*3*/
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          const Text('41'),
        ],
      ),
    );
  }
}

  1. 要使用行中所有剩余的可用空间,请使用 Expanded 小部件来拉伸 Column 小部件。要将列放置在行的开头,请将 crossAxisAlignment 属性设置为 CrossAxisAlignment.start
  2. 要在文本行之间添加间距,请将这些行放在 Padding 小部件中。
  3. 标题行以红色星形图标和文本 41 结尾。整个行位于 Padding 小部件内,并对每个边缘进行 32 像素的填充。

将应用主体更改为滚动视图

#

body 属性中,将 Center 小部件替换为 SingleChildScrollView 小部件。在 SingleChildScrollView 小部件中,将 Text 小部件替换为 Column 小部件。

dart
body: const Center(
  child: Text('Hello World'),
body: const SingleChildScrollView(
  child: Column(
    children: [

这些代码更新以以下方式更改应用。

  • SingleChildScrollView 小部件可以滚动。这允许不适合当前屏幕的元素显示。
  • Column 小部件按列出的顺序显示其 children 属性中的任何元素。children 列表中列出的第一个元素显示在列表的顶部。children 列表中的元素在屏幕上从上到下按数组顺序显示。

更新应用以显示标题部分

#

TitleSection 小部件作为 children 列表中的第一个元素添加。这将其放置在屏幕顶部。将提供的名称和位置传递给 TitleSection 构造函数。

dart
children: [
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
],

添加按钮部分

#

在本节中,添加将为你的应用添加功能的按钮。

**按钮**部分包含三个使用相同布局的列:一个图标位于一行文本之上。

The Button section as sketch and prototype UI
按钮部分作为草图和原型 UI

计划将这些列分布在一行中,以便每个列占据相同数量的空间。使用主颜色绘制所有文本和图标。

添加 ButtonSection 小部件

#

TitleSection 小部件之后添加以下代码,以包含构建按钮行的代码。

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

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    // ···
  }
}

创建一个用于制作按钮的小部件

#

由于每个列的代码可以使用相同的语法,因此创建一个名为 ButtonWithText 的小部件。该小部件的构造函数接受颜色、图标数据和按钮的标签。使用这些值,该小部件构建一个 Column,其中包含 Icon 和一个样式化的 Text 小部件作为其子元素。为了帮助分离这些子元素,Text 小部件被包装在一个 Padding 小部件中。

ButtonSection 类之后添加以下代码。

dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});
  // ···
}

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }

使用 Row 小部件定位按钮

#

将以下代码添加到 ButtonSection 小部件中。

  1. 添加 ButtonWithText 小部件的三个实例,每个按钮一个。
  2. 传递该特定按钮的颜色、Icon 和文本。
  3. 使用 MainAxisAlignment.spaceEvenly 值沿主轴对齐列。Row 小部件的主轴是水平的,Column 小部件的主轴是垂直的。因此,此值告诉 Flutter 在每个列之前、之间和之后沿 Row 均等地排列可用空间。
dart
class ButtonSection extends StatelessWidget {
  const ButtonSection({super.key});

  @override
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    return SizedBox(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ButtonWithText(
            color: color,
            icon: Icons.call,
            label: 'CALL',
          ),
          ButtonWithText(
            color: color,
            icon: Icons.near_me,
            label: 'ROUTE',
          ),
          ButtonWithText(
            color: color,
            icon: Icons.share,
            label: 'SHARE',
          ),
        ],
      ),
    );
  }
}

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    return Column(
      // ···
    );
  }
}

更新应用以显示按钮部分

#

将按钮部分添加到 children 列表中。

dart
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',
  ),
  ButtonSection(),
],

添加文本部分

#

在本节中,将文本描述添加到此应用中。

The text block as sketch and prototype UI
文本块作为草图和原型 UI

添加 TextSection 小部件

#

将以下代码作为 ButtonSection 小部件之后的单独小部件添加。

dart
class TextSection extends StatelessWidget {
  const TextSection({
    super.key,
    required this.description,
  });

  final String description;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Text(
        description,
        softWrap: true,
      ),
    );
  }
}

通过将 softWrap 设置为 true,文本行会在单词边界处换行之前填充列宽度。

更新应用以显示文本部分

#

添加一个新的 TextSection 小部件作为 ButtonSection 之后的子元素。添加 TextSection 小部件时,将其 description 属性设置为位置描述的文本。

dart
    location: 'Kandersteg, Switzerland',
  ),
  ButtonSection(),
  TextSection(
    description:
        'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
        'Bernese Alps. Situated 1,578 meters above sea level, it '
        'is one of the larger Alpine Lakes. A gondola ride from '
        'Kandersteg, followed by a half-hour walk through pastures '
        'and pine forest, leads you to the lake, which warms to 20 '
        'degrees Celsius in the summer. Activities enjoyed here '
        'include rowing, and riding the summer toboggan run.',
  ),
],

添加图像部分

#

在本节中,添加图像文件以完成你的布局。

配置你的应用以使用提供的图像

#

要配置你的应用以引用图像,请修改其 pubspec.yaml 文件。

  1. 在项目的顶部创建一个 images 目录。

  2. 下载 lake.jpg 图像并将其添加到新的 images 目录中。

  3. 要包含图像,请在应用根目录的 pubspec.yaml 文件中添加一个 assets 标签。当你添加 assets 时,它充当指向代码可用的图像的指针集。

    pubspec.yaml
    yaml
    flutter:
      uses-material-design: true
      assets:
        - images/lake.jpg

创建 ImageSection 小部件

#

在其他声明之后定义以下 ImageSection 小部件。

dart
class ImageSection extends StatelessWidget {
  const ImageSection({super.key, required this.image});

  final String image;

  @override
  Widget build(BuildContext context) {
    return Image.asset(
      image,
      width: 600,
      height: 240,
      fit: BoxFit.cover,
    );
  }
}

BoxFit.cover 值告诉 Flutter 使用两个约束来显示图像。首先,尽可能小地显示图像。其次,覆盖布局分配的所有空间,称为渲染框。

更新应用以显示图像部分

#

ImageSection 小部件作为 children 列表中的第一个子元素添加。将 image 属性设置为你在 配置你的应用以使用提供的图像 中添加的图像的路径。

dart
children: [
  ImageSection(
    image: 'images/lake.jpg',
  ),
  TitleSection(
    name: 'Oeschinen Lake Campground',
    location: 'Kandersteg, Switzerland',

恭喜

#

就是这样!当你热重载应用时,你的应用应该如下所示。

The finished app
完成后的应用

资源

#

你可以从以下位置访问本教程中使用的资源

Dart 代码: main.dart
图像: ch-photo
Pubspec: pubspec.yaml

下一步

#

要为此布局添加交互性,请遵循 交互性教程