跳转至正文

构建 Flutter 布局

学习如何在 Flutter 中构建布局。

本教程说明如何在 Flutter 中设计并构建布局。

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

The finished app.

The finished app.

图片来自 Unsplash 上的 Dino Reichmuth。文字来自 Switzerland Tourism

要更好理解布局机制,请先阅读 Flutter 的布局方法

考虑如何摆放用户界面各组件的位置。布局由这些摆放的最终结果构成。规划布局有助于加快编码。用视觉线索判断元素在屏幕上的位置会很有帮助。

你可用喜欢的方式,如界面设计工具或纸笔,在写代码前想好元素在屏幕上的位置。这是「量两次,裁一次」这句俗语在编程中的体现。

绘制布局草图

#

在本节中,考虑你希望为应用用户提供怎样的体验。

考虑如何摆放用户界面各组件的位置。布局由这些摆放的最终结果构成。规划布局有助于加快编码。用视觉线索判断元素在屏幕上的位置会很有帮助。

你可用喜欢的方式,如界面设计工具或纸笔,在写代码前想好元素在屏幕上的位置。这是「量两次,裁一次」这句俗语在编程中的体现。

  1. 用以下问题将布局分解为基本元素。

    • Can you identify the rows and columns?

    • Does the layout include a grid?

    • Are there overlapping elements?

    • Does the UI need tabs?

    • What do you need to align, pad, or border?

    • 能否识别出行与列?

    • 布局是否包含网格?

    • 是否有重叠元素?

    • UI 是否需要标签页?

    • 需要对齐、内边距或边框的是什么?

  2. 识别较大的元素。本例中,你将图片、标题、按钮和描述排成一列。

    Major elements in the layout: image, row, row, and text block

    Major elements in the layout: image, row, row, and text block

  3. 绘制每一行。

    1. 第 1 行 Title 区域有三个子节点:一列文字、星形图标和一个数字。其第一个子节点(列)包含两行文字,该列可能需要更多空间。

      Title section with text blocks and an icon

      Title section with text blocks and an icon

    2. 第 2 行 Button 区域有三个子节点:每个子节点包含一列,列内再有图标和文字。

      The Button section with three labeled buttons

      The Button section with three labeled buttons

绘制布局草图后,考虑如何编码实现。

你会把所有代码写在一个类里,还是为布局的每个部分各创建一个类?

遵循 Flutter 最佳实践时,为布局的每个部分创建一个类或 Widget。当 Flutter 需要重新渲染 UI 的某一部分时,只更新变化的最小部分。这就是 Flutter「万物皆 widget」的原因。若 Text widget 中只有文字变化,Flutter 只重绘该文字。Flutter 响应用户输入时尽可能少地改变 UI。

本教程中,将你识别的每个元素写成各自的 widget。

创建应用基础代码

#

本节搭建启动应用所需的基础 Flutter 应用代码。

  1. Set up your Flutter environment.

  2. Create a new Flutter app.

  3. Replace the contents of lib/main.dart with the following code. This app uses a parameter for the app title and the title shown on the app's appBar. This decision simplifies the code.

    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'),
            ),
          ),
        );
      }
    }
    
  1. 配置 Flutter 开发环境

  2. 创建新的 Flutter 应用

  3. lib/main.dart 的内容替换为以下代码。此应用使用参数设置应用标题以及在 appBar 中显示的标题,以简化代码。

添加标题区域

#

本节创建一个与下列布局相似的 TitleSection widget。

The Title section as sketch and prototype UI

The Title section as sketch and prototype UI

添加 TitleSection Widget

#

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. To use all remaining free space in the row, use the Expanded widget to stretch the Column widget. To place the column at the start of the row, set the crossAxisAlignment property to CrossAxisAlignment.start.

  2. To add space between the rows of text, put those rows in a Padding widget.

  3. The title row ends with a red star icon and the text 41. The entire row falls inside a Padding widget and pads each edge by 32 pixels.

  4. 要在行中使用所有剩余空闲空间,用 Expanded widget 拉伸 Column widget。要将列放在行首,将 crossAxisAlignment 设为 CrossAxisAlignment.start

  5. 要在文本行之间添加间距,将这些行放在 Padding widget 中。

  6. 标题行以红色星形图标和文本 41 结束。整行位于 Padding widget 内,四边各留 32 像素内边距。

将应用 body 改为可滚动视图

#

body 属性中,将 Center widget 替换为 SingleChildScrollView widget。在 SingleChildScrollView widget 内,将 Text widget 替换为 Column widget。

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

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

  • A SingleChildScrollView widget can scroll. This allows elements that don't fit on the current screen to display.

  • A Column widget displays any elements within its children property in the order listed. The first element listed in the children list displays at the top of the list. Elements in the children list display in array order on the screen from top to bottom.

  • SingleChildScrollView widget 可以滚动,使放不进当前屏幕的元素得以显示。

  • Column widget 按 children 属性中列出的顺序显示其中的元素。children 列表中的第一项显示在顶部,列表中的元素按数组顺序自上而下显示在屏幕上。

更新应用以显示标题区域

#

TitleSection widget 作为 children 列表的第一项添加,使其显示在屏幕顶部。将提供的名称和位置传给 TitleSection 构造函数。

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

添加按钮区域

#

本节添加为应用增加功能的按钮。

Button 区域包含三列,使用相同布局:图标在上、文字在下。

The Button section as sketch and prototype UI

The Button section as sketch and prototype UI

计划将这些列放在一行中,使每列占用相同空间。将所有文字和图标绘制为主题主色。

添加 ButtonSection widget

#

TitleSection widget 之后添加以下代码,用于构建按钮行。

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

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

}

创建用于制作按钮的 widget

#

由于每列代码可使用相同写法,创建一个名为 ButtonWithText 的 widget。其构造函数接受颜色、图标数据和按钮标签。widget 用这些值构建包含 Icon 和样式化 Text widget 的 Column。为分隔子节点,用 Padding widget 包裹 Text widget。

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 widget 摆放按钮

#

将以下代码添加到 ButtonSection widget 中。

  1. Add three instances of the ButtonWithText widget, once for each button.

  2. Pass the color, Icon, and text for that specific button.

  3. Align the columns along the main axis with the MainAxisAlignment.spaceEvenly value. The main axis for a Row widget is horizontal and the main axis for a Column widget is vertical. This value, then, tells Flutter to arrange the free space in equal amounts before, between, and after each column along the Row.

  4. 为每个按钮各添加一个 ButtonWithText widget 实例。

  5. 传入该按钮对应的颜色、Icon 和文字。

  6. MainAxisAlignment.spaceEvenly 沿主轴对齐各列。Row widget 的主轴是水平的,Column widget 的主轴是垂直的。该值告诉 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

The text block as sketch and prototype UI

添加 TextSection widget

#

ButtonSection widget 之后作为独立 widget 添加以下代码。

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 时,文字行会先填满列宽,再在词边界处换行。

更新应用以显示文本区域

#

ButtonSection 之后添加新的 TextSection widget 作为子节点。添加 TextSection widget 时,将其 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. Create an images directory at the top of the project.

  2. 在项目顶层创建 images 目录。

  3. Download the lake.jpg image and add it to the new images directory.

  4. 下载 lake.jpg 图片并添加到新的 images 目录。

  5. To include images, add an assets tag to the pubspec.yaml file at the root directory of your app. When you add assets, it serves as the set of pointers to the images available to your code.

  6. 要包含图片,在应用根目录的 pubspec.yaml 文件中添加 assets 标签。添加 assets 后,它作为代码可用图片的指针集合。

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

创建 ImageSection widget

#

在其他声明之后定义以下 ImageSection widget。

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 在两项约束下显示图片:首先尽可能小地显示图片;其次覆盖布局分配的全部空间,即 render box。

更新应用以显示图片区域

#

ImageSection widget 作为 children 列表的第一项添加。将 image 属性设为你于配置应用以使用提供的图片中添加的图片路径。

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

恭喜

#

就是这样!热重载应用后,应用应如下所示。

The finished app

The finished app

资源

#

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

Dart code: main.dart
Image: ch-photo
Pubspec: pubspec.yaml

下一步

#

要为该布局添加交互性,请参阅交互性教程