MDC (Material Components) 帮助开发者实现 Material Design。 MDC 由谷歌的工程师和交互团队联合呈现,其具备数种精美的 UI 组件,适用于 Android、iOS、Web 和 Flutter。 查看详细:material.io/develop |
在 Codelab MDC-101 中,你已经使用两种 Material 组件构建了一个登录页面,也就是文本框和带有 ink 波纹效果的按钮。
在本期 Codelab 中,你将为一个 Shrine 这个售卖衣物和家庭用品的电商应用构建主界面。它包含:
我们最终会构建一个卖衣服和家居的电子商务应用 - SHRINE,总共通过四个 Codelab 搞定它,本 Codelab 是其中的第二个,相关的 Codelab 如下: | 在 MDC Flutter 教程 4 结束时,你将有机会做出一个这样的应用: |
你需要安装两部分来完成本次实验,Flutter 的 SDK 和编辑器(editor),这个 codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。
你可以通过如下任何设备完成本 codelab:
如果你已经完成了 MDC Flutter 教程 1:Material 组件基础(MDC 101),你可以跳过这一节,直接从下一节继续......
此起步项目位于 material-components-flutter-Codelabs-102-starter_and_101-complete/mdc_100_series
目录
使用如下命令来从 GitHub 克隆此 Codelab :
git clone https://github.com/material-components/material-components-flutter-Codelabs.git cd material-components-flutter-Codelabs git checkout 102-starter_and_101-complete
设置你的工程
如下设置工程的方法是使用 Android Studio 做演示的。
1. 在你的命令行工具里,切换到这个文件夹 | |
2. 执行命令 |
1. 打开 Android Studio | |
2. 当你看到欢迎界面时候,选择打开一个已存在的工程(Open an existing Android Studio project.) | |
3. 打开这个文件夹 如果此时提示一些错误请忽略,等第一次成功编译之后应该会好一些。 | |
4. 在项目面板左侧,删除测试文件 | |
5. 如果提示"升级平台和插件"或要配置 FlutterRunConfigurationType,请重启 Android Studio。 |
运行起步应用
下面我们以在 Android 模拟器或真机上为例做如下的步骤示范,当然如果你装有 Xcode,你也可以在 iOS 模拟器或真机上进行尝试。
1. 选择设备或模拟器 如果 Android 模拟器还没有运行起来,请选择 Tools -> Android -> AVD Manager 先进行 创建并启动模拟器 的操作。如果 AVD 里已经有创建好的模拟器,你可以直接在 IntelliJ 的设备选择列表里启动模拟器,如下一步所示。 (对于 iOS 模拟器,如果你还没有启动,请在你的电脑上选择 Flutter Device Selection -> Open iOS Simulator 来启动。) | |
2. 运行你的 Flutter 应用:
|
一切就绪后,在模拟器或真机中你应该可以看到 Shrine 的登录页面,这是我们在 MDC-101 Codelab 中所完成的工作。
现在登录页看起来还不错。让我们为应用再充实一下。
退出登录界面以后,就会显示出主界面,上面写有一行 "You did it!" 。这很不错!但目前对用户来说,他们还做不了什么,也不知道当前在哪个页面上。为解决这些问题,是时候要来添加导航了。
Material Design 为导航提供了相关的规范,使其有足够高的可用度。其中最常见的组件即应用顶部栏。
让我们来添加应用顶部栏,以提供导航和其他操作的快捷入口。
在 home.dart
中,为 Scaffold 添加 AppBar :
return Scaffold(
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
),
把 AppBar 添加到 Scaffold 的 appBar
中,我们很轻松得就得到了一个完美的布局,它把 AppBar 保持在顶部,而 body 在下面。
保存项目。当 Shrine 更新后,点击 Next 以观察一下主界面。
看起来 AppBar 已经有了,但还需要一个标题。
在 home.dart
中,为 AppBar 添加一个标题:
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
title: Text('SHRINE'),
// TODO: Add trailing buttons (102)
保存项目。
很多应用顶部栏在标题旁边都会有按钮。让我们在应用中添加一个菜单图标吧。
依旧是在 home.dart
中,为 AppBar 的 leading
字段设置一个 IconButton 。(需要将其放在 title
字段前面,因为它们遵循从头至尾 leading-to-trailing 的先后顺序):
return Scaffold(
appBar: AppBar(
// TODO: Add buttons and title (102)
leading: IconButton(
icon: Icon(
Icons.menu,
semanticLabel: 'menu',
),
onPressed: () {
print('Menu button');
},
),
保存项目。
菜单图标(也被称为 "汉堡图标")就出现在了那里,正如你所期待的那样。
你也可以在标题尾部添加按钮。在 Flutter 中它们被称为 "actions" 。
我们还有空间放下两个 IconButtons 。
把它们添加到 AppBar 实例中,title 之后:
// TODO: Add trailing buttons (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');
},
),
],
保存项目。你的主界面现在看起来应该是这样:
现在应用有了头部的按钮,标题,以及右边的两个 actions 。应用顶部栏还显示出了 elevation ,它以一种浅阴影的形式来表示它和其他内容在不同的层级上。
现在应用有一定的结构了,让我们以卡片的形式来组织内容吧。
让我们从在顶部栏下方添加一个卡片开始吧。单凭 Card widget 不足以展示出我们想要的效果,我们要把它放进一个 GridView widget 中。
在 Scaffold 的 body 里,将 Center 替换为 GridView :
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
// TODO: Build a grid of cards (102)
children: <Widget>[Card()],
),
我们来解读一下代码。GridView 中的条目是有限的而不是无穷多个,所以需要调用 count()
。但还要一些信息来定义其布局。
crossAxisCount
:指定每横行展示多少条目。这里我们想要两列。
padding
: 给 GridView 的四周都增加了空间。当然你目前还看不到 GridView 尾部或底部新增加的空间,因为还没有足够多的子 view 被添加进来。
childAspectRatio
: 以宽高比(宽除以高)的形式定义了条目的大小。
GridView 里每个条目的大小默认都是一样的。
总的来说, GridView 以如下方式计算各个子项的宽度:([width of the entire grid] - [left padding] - [right padding]) / number of columns
。代入我们已有的值,得到:([width of the entire grid] - 16 - 16) / 2
。
基于宽度、宽高比计算出高度:([width of the entire grid] - 16 - 16) / 2 * 9 / 8
。因为是先得到宽之后再计算高,所以我们把 8 和 9 除法关系颠倒了一下。
现在我们有了一个卡片,但它里面是空的。让我们来给它里面加入 widget 吧。
卡片里需要有一张图片、一个标题和一段文字。
更新一下 GridView 的子项:
// TODO: Build a grid of cards (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'),
],
),
),
],
),
)
],
这段代码添加了一个 Column widget ,使子项可以垂直排列。
crossAxisAlignment
字段设为了 CrossAxisAlignment.start
,意思是"将文本向头部对齐"。
图片形状由 AspectRatio 决定,而不是提供的图片本身的形状。
Padding 则使得文本从边缘向中间移动一点。
两个 Text widgets 上下放置,用 SizedBox 来表示它们之间有 8 points 的距离。我们在 Padding 之中创建了一个 Column 来放置它们。
保存项目:
在预览里,你会看到这张卡片被靠边放置,它带有圆角、阴影(表现出卡片的 elevation )。这种外形在 Material 里叫 "container" 。(不要与 Container widget 混淆。)
卡片通常放在一起用于展示集合。让我们来把它们以集合的形式布局到一个网格里。
每当多个卡片在同一界面呈现时,它们都会被以一个或多个集合的形式组织起来。同一集合中的卡片处于相同的水平面,即它们静止时 evelation 相同(除非卡片被选中或者拖动,我们在这里还不会这么做)。
目前与卡片相关的多有代码都写在了 GridView 的 children
里。这样会有很多嵌套的代码,难以阅读。让我们将它们抽象到一个函数里,使它可以生成任意数量的卡片,最后以卡片列表的形式返回。
在 build()
之后添加一个私有函数(注意以单下划线开头的函数即是私有的):
// TODO: Make a collection of cards (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;
}
把这个函数返回的卡片列表传递给 GridView 的 children
字段。注意要替换掉 GridView children
里之前所有的代码:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
children: _buildGridCards(10) // Replace
),
保存项目:
卡片出现了,但它们还没有展示任何东西。现在该加上一些产品数据上去了。
应用里包含产品图片、名称、价格。让我们把这些内容加到每个卡片里吧。
接下来,在 home.dart
中,引入一个新的包和我们已经为你准备好的数据文件:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'model/products_repository.dart';
import 'model/product.dart';
最后,更改 _buildGridCards()
方法来获取产品信息数据,然后在卡片中使用这些数据:
// TODO: Make a collection of cards (102)
// Replace this entire method
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: Adjust card heights (103)
child: Column(
// TODO: Center items on the card (103)
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18 / 11,
child: Image.asset(
product.assetName,
package: product.assetPackage,
// TODO: Adjust the box size (102)
),
),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
// TODO: Align labels to the bottom and center (103)
crossAxisAlignment: CrossAxisAlignment.start,
// TODO: Change innermost Column (103)
children: <Widget>[
// TODO: Handle overflowing labels (103)
Text(
product.name,
style: theme.textTheme.title,
maxLines: 1,
),
SizedBox(height: 8.0),
Text(
formatter.format(product.price),
style: theme.textTheme.body2,
),
],
),
),
),
],
),
);
}).toList();
}
注意:先不要编译运行。我们还有一个小改动。
在编译之前,更改一下 build()
方法 ,把 BuildContext 实例传给 _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 points 的 padding 。
保存项目:
产品数据展示出来了,但是图片周围有多余的空间。图片是由 BoxFit 进行绘制的,(在这个案例中)默认是 .scaleDown
模式。我们把它改为 .fitWidth
以让图片填充,去掉多出的空白。
更改一下图片的 fit
字段:
// TODO: Adjust the box size (102)
fit: BoxFit.fitWidth,
我们的产品就这样完美地呈现在应用之中了!
我们的应用已经拥有了基本的流程,它把用户从登录界面带到主界面,在这里用户可以浏览产品。我们用了数行代码中,便添加了应用顶部栏(带有标题和三个按钮)和卡片(展示应用的内容)。我们的主界面现在简洁易用,结构简单,并具备一些可以进行交互的内容。
我们现在已经用过 MDC-Flutter 库中的四个核心组件了!有应用顶部栏,卡片,文本框和按钮。访问 Flutter Widgets Catalog 来探索更多的组件。
虽然功能已经有了,但我们的应用还没有表现出任何品牌或是意图。在 MDC-103: Material Design Theming with Color, Shape, Elevation and Type 中,我们将定制这些组件的样式,以表现出一个现代的、充满活力的品牌形象。