[译] MDC-102 Flutter:Material 结构和布局(Flutter)

8,842 阅读17分钟

MDC-102 Flutter:Material 结构和布局(Flutter)

1. 介绍

Material Components(MDC)帮助开发者实现 Material Design。MDC 由谷歌团队的工程师和 UX 设计师创造,为 Android、iOS、Web 和 Flutter 提供很多美观实用的 UI 组件。

material.io/develop

在教程 MDC-101 中,你使用了两个 Material 组件:文本框和墨水波纹效果的按钮来构建一个登陆页面。现在让我们通过添加导航、结构和数据来拓展应用。

你将要构建

在本教程中,你将为 Shrine —— 一个销售服装和家居用品的电子商务应用程序构建一个主页面。它将含有:

  • 一个位于顶部的应用栏
  • 一个由产品填充的网格列表

这是四篇教程里的第二篇,它将引导你为 Shrine 的产品构建应用程序。我们建议你按照教程的顺序一步一步地编写你的代码。

相关的教程可以在以下位置找到:

到 MDC-104 的最后,你将会构建一个像这样的应用:

将要用到的 MDC 组件

  • 顶部应用栏(Top app bar)
  • 网格(Grid)
  • 卡片(Card)

本教程中,你将使用 MDC-Flutter 提供的默认组件。你将会在 MDC-103: Material Design Theming 的颜色、形状、高度和类型中学习如何定制它们。

你将需要

  • Flutter SDK
  • 安装好 Flutter 插件的 Android Studio,或者你喜欢的代码编辑器
  • 示例代码

要在 iOS 上构建和运行 Flutter 应用程序,你需要满足以下要求:

  • 运行 macOS 的计算机
  • Xcode 9 或更新版本
  • iOS 模拟器,或者 iOS 物理设备

要在 Android 上构建和运行 Flutter 应用程序,你需要满足以下要求:

  • 运行 macOS、Windows 或 Linux 的计算机
  • Android Studio
  • Android 模拟器(随 Android Studio 一起提供)或 Android 物理设备

2. 安装 Flutter 环境

前提条件

要开始使用 Flutter 开发移动应用程序,你需要:

  • Flutter SDK
  • 装有 Flutter 插件的 IntelliJ IDE,或者你喜欢的代码编辑器

Flutter 的 IDE 工具适用于 Android StudioIntelliJ IDEA Community(免费)和 IntelliJ IDEA Ultimate

要在 iOS 上构建和运行 Flutter 应用程序,你需要满足以下要求:

  • 运行 macOS 的计算机
  • Xcode 9 或更新版本
  • iOS 模拟器,或者 iOS 物理设备

要在 Android 上构建和运行 Flutter 应用程序,你需要满足以下要求:

  • 运行 macOS、Windows 或者 Linux 的计算机
  • Android Studio
  • Android 模拟器(随 Android Studio 一起提供)或 Android 物理设备

获取详细的 Flutter 安装信息

重要提示:如果连接到计算机的 Android 手机上出现“允许 USB 调试”对话框,请启用始终允许从此计算机选项,然后单击确定

在继续本教程之前,请确保你的 SDK 处于正确的状态。如果之前安装过 Flutter,则使用 flutter upgrade 来确保 SDK 处于最新版本。

flutter upgrade

运行 flutter upgrade 将自动运行 flutter doctor。如果这是首次安装 Flutter 且不需升级,那么请手动运行 flutter doctor。查看显示的所有检查标记;这将会下载你需要的任何缺少的 SDK 文件,并确保你的计算机配置无误以进行 Flutter 的开发。

flutter doctor

3. 下载教程初始应用程序

从 MDC-101 继续?

如果你完成了 MDC-101,那么本教程所需代码应该已经准备就绪,跳转到 添加应用栏 步骤。

从头开始?

下载初始应用程序

下载初始程序

此入门程序位于 material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series 目录中。

...或者从 GitHub 克隆它

要从 GitHub 克隆此项目,请运行以下命令:

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs
git checkout 102-starter_and_101-complete

更多帮助:从 GitHub 上克隆存储库

正确的分支

教程 MDC-101 到 104 连续构建。所以当你完成 102 的代码后,它将变成 103 教程的初始代码!代码被分成不同的分支,你可以使用以下命令将它们全部列出:

git branch --list

要查看完整代码,请切换到 103-starter_and_102-complete 分支。

建立你的项目

以下步骤默认你使用的是 Android Studio (IntelliJ)。

创建项目

  1. 在终端中,导航到 material-components-flutter-codelabs

  2. 运行 flutter create mdc_100_series

打开项目

  1. 打开 Android Studio。

  2. 如果你看到欢迎页面,单击 打开已有的 Android Studio 项目

  1. 导航到 material-components-flutter-codelabs/mdc_100_series 目录并单击打开,这将打开此项目。

在构建项目一次之前,你可以忽略在分析中见到的任何错误。

  1. 在左侧的项目面板中,删除测试文件 ../test/widget_test.dart

  1. 如果出现提示,安装所有平台和插件更新或 FlutterRunConfigurationType,然后重新启动 Android Studio。

提示:确保你已安装 Flutter 和 Dart 插件

运行初始程序

以下步骤默认你在 Android 模拟器或设备上进行测试。你也可以在 iOS 模拟器或设备上进行,只要你安装了 Xcode。

  1. 选择设备或模拟器

如果 Android 模拟器尚未运行,请选择 Tools -> Android -> AVD Manager创建您设备并启动模拟器。如果 AVD 已存在,你可以直接在 IntelliJ 的设备选择器中启动模拟器,如下一步所示。

(对于 iOS 模拟器,如果它尚未运行,通过选择 Flutter Device Selection -> Open iOS Simulator 来在你的开发设备上启动它。)

  1. 启动 Flutter 应用:
  • 在你的编辑器窗口顶部寻找 Flutter Device Selection 下拉菜单,然后选择设备(例如,iPhone SE / Android SDK built for )。
  • 点击运行图标(
    )。

如果你无法成功运行此应用程序,停下来解决你的开发环境问题。尝试导航到 material-components-flutter-codelabs;如果你在终端中下载 .zip 文件,导航到 material-components-flutter-codelabs-... 然后运行 flutter create mdc_100_series

成功!Shrine 的初始登陆代码应该在你的模拟器中运行了。你可以看到 Shrine 的 logo 和它下面的名称 "Shrine"。

现在登录页面看起来不错,让我们用一些产品来填充应用。

4. 添加顶部应用栏

当登陆页面消失时主页面将出现并显示“你做到了!”。这很棒!但是我们的用户不知道能做什么操作,也不知道现在位于应用何处,为了解决这个问题,是时候添加导航了。

导航 是指允许用户在应用中移动的组件、交互、视觉提示和信息结构。它使得内容和功能更加注目,任务也因此易于完成。

在 Material 指南中了解更多有关导航的信息。

Material Design 提供确保高度可用性的导航模式,其中最注目的组件就是顶部应用栏。

你可以将顶部应用栏当作 iOS 中的“导航栏”,或者简单看成一个 “App Bar” 或 “Header”。

要提供导航并让用户快速访问其他操作,让我们添加一个顶部应用栏。

添加应用栏部件

home.dart 中,将应用栏添加到 Scaffold 中:

return Scaffold(
  // TODO: 添加应用栏(102)
  appBar: AppBar(
    // TODO: 添加按钮和标题(102)
  ),

AppBar 添加到 Scaffold 的 appBar: 字段位置,为了我们完美的布局,让应用栏保持在页面的顶部或底部。

Scaffold 在中是一个重要的部件。它为像抽屉、snack bar 和 bottom sheet 等各种常见 Material 组件提供方便的 API。它甚至可以帮助布置一个 Floating Action Button。

Flutter 文档中了解更多有关 Scaffold 的信息。

保存项目,当 Shrine 应用更新后,单击 Next 来查看主屏幕。

应用栏看起来不错,但它还需要一个标题。

如果应用没有更新,再次单击 “Play” 按钮,或者点击 “Play” 后的 “Stop”。

添加文本部件

home.dart 中,给应用栏添加一个标题:

// TODO: 添加应用栏(102)  
  appBar: AppBar(
    // TODO: 添加按钮和标题(102)

    title: Text('SHRINE'),
        // TODO:添加后续按钮(102)

保存项目。

到目前为止,你应该已经注意到我们所说的“平台差异”了。Material 明白 Android、iOS、Web 各平台都有差异。用户对他们有不同的期望。举例来说,在 iOS 里标题几乎总是居中的,这是 UIKit 提供的默认配置。在 Android 上标题是左对齐的。所以如果你使用的是 Android 模拟器或设备,那么标题应该位于左侧,对于 iOS 模拟器和设备而言,它应该是居中的。

了解更多信息,请查参阅有关跨平台适配的 Material 文章

许多应用栏在标题旁边都设有按钮,让我们在应用中添加一个菜单图标。

添加位于首部的图标按钮

还是在 home.dart 中,在 AppBar 的 leading 字段设置一个图标按钮:(放在 title: 字段前,按照部件从首到尾的顺序):

return Scaffold(
  appBar: AppBar(
    // TODO: 添加按钮和标题(102)
    leading: IconButton(
      icon: Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

保存项目。

菜单图标(也被称作“汉堡包”)会在你期望的位置显示出来。

IconButton 类是在你的应用里引入 Material 图标的快捷方式。它有一个 Icon 部件。 Flutter 在 Icons 类里有整套的图标。它会根据字符串常量的映射自动导入图标。

Flutter 文档中了解更多有关 Icons 类的信息。有关 Icon 部件的信息请阅读这个 Flutter 文档

你也可以在标题尾部添加按钮。在 Flutter 中,它们被称为 "action"。

Leading(首部)trailing(尾部) 是表达方向的术语,指的是与语言无关的文本行的开头和结尾。当使用一个像英语这样的 LTR(左到右)语言时, leading 意味着 左侧trailing 代表着 右侧。在像阿拉伯语这样的 RTL(右到左)语言时, leading 意味着 右侧trailing 代表着 左侧

了解 UI 镜像的更多信息,请参阅 双向性 Material Design 准则。

添加 action

还有两个 IconButton 的空间。

在 AppBar 实例中的标题后面添加它们:

// TODO: 添加尾部按钮(102)
actions: <Widget>[
  IconButton(
    icon: Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

保存你的项目。你的主屏幕看起来应该像这样:

现在这个应用在左侧有一个按钮、一个标题,右侧还有两个 action。应用栏还利用阴影显示高度,表示它与内容处于不同的层级。

在 Icon 类中,SemanticLabel 字段是在 Flutter 中添加辅助功能信息的常用方法。这很像 Android 的 Content Label 或 iOS 的 UIAccessibility accessibilityLabel。你会在很多类中见到它。

这个字段的信息很好地向使用屏幕阅读器的人说明了该按钮的作用。

对于没有 semanticLabel: 字段的部件,你可以将其包装在 Semantics 部件中,在其 Flutter 文档中了解更多有关的信息。

5. 在网格中添加卡片

现在我们的应用像点样子了,让我们接着放置一些卡片来组织内容。

卡片 是显示单体内容和动作的独立的元素。它们是一种可以灵活地呈现近似内容集合的方式。

在 Material 指南有关卡片的文章中了解更多信息。

要了解卡片部件,请参阅在 Flutter 中构建布局

添加网格视图

让我们从应用栏底部添加一个卡片开始。单一的 卡片 部件不足以让我们将它放到我们想要的位置,所以我们需要将它封装在一个 网格视图 中。

用 GridView 替换 Scaffold 中 body 字段的 Center:

// TODO: 添加网格视图(102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: 构建一组卡片(102)
  children: <Widget>[Card()],
),

让我们分析这段代码。网格视图调用 count() 构造函数,因要添加的项目数是可数的而不是无限的。但它需要更多信息来定义其布局。

crossAxisCount: 指定横向显示数目,我们设置成 2 列。

Flutter 中的 Cross axis(横轴) 表示非滚动轴。可滚动的方向称为 主轴。所以如果你的应用像网格视图默认的那样垂直滚动,那么横轴就是水平方向。

详情请参阅构建布局

padding: 字段为网格视图的 4 条边设置填充。当然你现在看不到首尾的填充,因为网格视图内还没有其他子项。

childAspectRatio: 字段依据宽高比确定其大小。

默认地,网格视图中的项目尺寸相同。

将这些加在一起,网格视图按照如下方式计算每个子项的宽度:([整个网格宽度] - [左填充] - [右填充]) / 列数。在这里就是:([整个网格宽度] - 16 - 16) / 2

高度是根据宽度计算得来的,通过应用宽高比:([整个网格宽度] - 16 - 16) / 2 * 9 / 8。我们翻转了 8 和 9,因为我们是用宽度来计算高度。

我们已经有了一个空的卡片了,让我们添加一些子部件到卡片中。

布局内容

卡片内应该包含一张图片、一个标题和一个次级文本。

更新网格视图的子项:

// TODO: 构建一组卡片(102)
children: <Widget>[
  Card(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

这段代码添加了一个列部件,用来垂直地布局子部件。

crossAxisAlignment: 字段指定 CrossAxisAlignment.start 属性,这意味着“文本与前沿对齐”。

AspectRatio 部件决定图像的形状,无论提供的是何种图像。

Padding 使得文本与边框保持一定距离。

两个 Text 部件垂直堆叠,在其间保持 8 个单位的间隔(SizedBox)。我们使用另一个 Column 来把它们放到 Padding 中。

保存你的项目:

在这个预览里,你可以看到卡片从边缘置入,并带有圆角和阴影(这代表着卡片的高度)。整个形状在 Material 中被称为 “container(容器)”。(不要与名为 Container 的实际部件类混淆。)

除了容器以外,在 Material 中卡片内所有的元素实际上都是可选的。你可以添加标题文本、缩略图、头像或者小标题文本、分隔符甚至是按钮和图标。

了解更多消息,请参阅 Material 指南上有关卡片的文章。

卡片经常以集合的形式和其他卡片一起出现,让我们在网格视图中给它们布局。

6. 生成卡片集合

每当屏幕上出现多张卡片时,它们就会组成一个或多个集合。集合中的卡片是共面的,这意味着卡片共享相同的静止高度。(除了卡片被拾起或拖动,但在这里我们不会这么做。)

将卡片添加到集合

现在我们的卡片是网格视图内的 children: 字段子项。这有一大段难以阅读的嵌套代码。让我们将它提取到一个函数中来生成任意数量的空卡片,然后返回给我们。

// TODO: 生成卡片集合(102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) => Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18.0 / 11.0,
            child: Image.asset('assets/diamond.png'),
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text('Title'),
                SizedBox(height: 8.0),
                Text('Secondary Text'),
              ],
            ),
          ),
        ],
      ),
    ),
  );

  return cards;
}

将生成的卡片分配给网格视图的 children 字段。记得用新代码替换网格视图中的所有内容。

// TODO: 添加网格视图(102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // 替换所有内容
),

保存你的项目:

卡片已经在这了,但它们什么都没有显示。现在是时候添加一些产品数据了。

###添加产品数据

这个应用中的产品有着图像、名称和价格。让我们把这些添加到已有的卡片部件中。

然后,在 home.dart 中,导入数据模型需要的新包和文件:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/products_repository.dart';
import 'model/product.dart';

最后,更改 _buildGridCards() 来获取产品信息,并将数据应用到卡片中:

// TODO: 生成卡片集合(102)

// 替换整个方法
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products == null || products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      // TODO: 调整卡片高度(103)
      child: Column(
        // TODO: 卡片的内容设置居中(103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: 调整盒子尺寸(102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: 标签底部对齐并居中(103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: 更改最内部的列(103)
                children: <Widget>[
                 // TODO: 处理溢出的标签(103)
                 Text(
                    product.name,
                    style: theme.textTheme.title,
                    maxLines: 1,
                  ),
                  SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.body2,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

注意:应用现在无法编译和运行,我们还需要进行修改。

要设置文本的样式,我们使用当前 BuildContext 中的 ThemeData

了解有关文本样式的更多信息,请参阅 Material 指南中的排版一文。了解有关主题的更多信息,请参考教程下一章 MDC-103: Material Design Theming 的颜色、形状、高度和类型

在尝试编译之前,将 BuildContext 传入 build() 方法中的 _buildGridCards()

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

你可能注意到了我们没有在卡片间添加任何垂直的间隔,这是因为在其顶部与底部默认有 4 个单位的填充。

保存你的项目:

产品的数据显示出来了,但是图像四周有额外的空间。图像默认依据 .scaleDownBoxFit 绘制(在这个情况下)。让我们将其更改为 .fitWidth 来让它们放大一点,删除多余的空间。

修改图像的 fit: 字段:

  // TODO: 调整盒子尺寸(102)
  fit: BoxFit.fitWidth,

现在我们的产品完美的展现在应用中了!

7. 总结

我们的应用已经有了基本的流程,将用户从登陆屏幕带到可以查看产品的主屏幕。通过几行代码,我们添加了一个顶部应用栏(带有标题和三个按钮)以及卡片(用于显示我们应用的内容)。我们的主屏幕简洁实用,具有基本的结构和可操作的内容。

完成的 MDC-102 应用可以在 103-starter_and_102-complete 分支中找到。

你可以用此分支下的应用来对照验证你的版本。

下一步

通过顶部应用栏、卡片、文本框和按钮,我们已经使用了 MDC-Flutter 库中的四个核心组件!你可以访问 Flutter 部件目录来探索更多组件。

虽然它完全正常运行,我们的应用尚未表达任何特殊的品牌特点。在 MDC-103: Material Design Theming 的颜色、形状、高度和类型中,我们将定制这些组件的样式,来诠释一个充满活力的、现代的品牌。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏