1. 简介
Material Components (MDC) 可帮助开发者实施 Material Design。MDC 由 Google 的工程师和用户体验设计师团队创建,拥有数十个精美而强大的界面组件,可用于 Android、iOS、Web 和 Flutter。material.io/develop |
现在,您可以比以往更广泛地使用 MDC 来自定义应用程序的独特风格。Material Design 近期的扩展可以让设计师和开发者更灵活地表达其产品的品牌。
在 Codelab MDC-101 和 MDC-102 中,您使用 Material Components (MDC) 构建了一个名为 Shrine 的应用(一个销售服饰和家居用品的电子商务应用)的基础内容。此应用包含一个用户流程,它从登录屏幕开始,然后将用户带到显示产品的主屏幕。
您将构建的应用
在本 Codelab 中,您将使用以下组件自定义 Shrine 应用:
- 颜色
- 字体
- 高度
- 形状
- 布局
本 Codelab 中的 MDC-Flutter 组件和子系统
- 主题
- 字体
- 高度
- 图像列表
您如何评价您在 Flutter 开发方面的经验?
2. 设置您的 Flutter 环境
准备工作
要开始使用 Flutter 开发移动应用,您需要执行以下操作:
- 下载并安装 Flutter SDK。
- 更新 PATH 以包含 Flutter SDK。
- 安装 Android Studio 并添加 Flutter 和 Dart 插件,或者选择您喜欢的编辑器。
- 安装 Android 模拟器、iOS 模拟器(需要装有 Xcode 的 Mac),或者使用实体设备。
如需了解有关 Flutter 安装的详细信息,请参阅 使用入门:安装。要设置编辑器,请参阅 使用入门:设置编辑器。安装 Android 模拟器时,您可以使用默认选项(如 Pixel 3 手机)并安装最新系统映像。建议启用 VM 加速,但不做强制要求。在完成上述 4 个步骤后,您可以返回到本 Codelab。要完成本 Codelab,您只需要安装适用于其中一个平台(Android 或 iOS)的 Flutter。
确保 Flutter SDK 处于正确的状态
在继续本 Codelab 之前,请确保您的 SDK 处于正确的状态。如果之前安装了 Flutter SDK,请使用 flutter upgrade
确保 SDK 处于最新状态。
flutter upgrade
运行 flutter upgrade
会自动运行 flutter doctor。如果这是全新的 Flutter 安装,则无需 升级,手动运行 flutter doctor
即可。它将报告是否存在为完成设置您需安装的任何依赖项。您可以忽略与您无关的对勾标记(例如,如果您不打算针对 iOS 开发,则可以忽略 Xcode)。
flutter doctor
常见问题解答
3. 下载 Codelab 起始应用
接续 MDC-102?
如您完成了 MDC-102,那么您的代码应该已经可以用于此 Codelab。请跳到以下步骤: 更改颜色。
从头开始?
下载起始 Codelab 应用
起始应用位于 material-components-flutter-codelabs-103-starter_and_102-complete/mdc_100_series
目录中。
......或者从 GitHub 克隆
要从 GitHub 克隆此 Codelab,请运行以下命令:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 103-starter_and_102-complete
设置项目
以下说明假定您使用的是 Android Studio (IntelliJ)。
打开项目
1.打开 Android Studio。 |
2.如果看到欢迎屏幕,请点击 Open an existing Android Studio project(打开现有的 Android Studio 项目)。 |
3.导航至 |
4.如果出现提示:
然后重新启动 Android Studio。 |
运行起始应用
以下说明假定您是在 Android 模拟器或设备上进行的测试,但如果您安装了 Xcode,也可以在 iOS 模拟器或设备上测试。
1.选择设备或模拟器。如果 Android 模拟器尚未运行,请选择 Tools(工具)-> Android -> AVD Manager(AVD 管理器)以创建虚拟设备,然后启动模拟器。如果 AVD 已存在,您可以直接从 Android Studio 中的设备选择器启动模拟器,如下一步中所示。(对于 iOS 模拟器,如果它尚未运行,则通过选择 Flutter Device Selection(Flutter 设备 |
2.启动 Flutter 应用:
|
成功!您应该会在模拟器中看到上一个 Codelab 中的 Shrine 登录页面。
Android | iOS |
点击"Next"(下一步)可看到上一个 Codelab 中的主页。
Android | iOS |
4. 更改颜色
设计师已创建出代表 Shrine 品牌的配色方案,并希望您在 Shrine 应用中实施该配色方案。
首先,让我们将这些颜色导入我们的项目。
创建 colors.dart
在 lib
中新建一个名为 colors.dart
的 dart 文件。导入 Material Components 并添加 const 颜色值:
import 'package:flutter/material.dart';
const kShrinePink50 = Color(0xFFFEEAE6);
const kShrinePink100 = Color(0xFFFEDBD0);
const kShrinePink300 = Color(0xFFFBB8AC);
const kShrinePink400 = Color(0xFFEAA4A4);
const kShrineBrown900 = Color(0xFF442B2D);
const kShrineErrorRed = Color(0xFFC5032B);
const kShrineSurfaceWhite = Color(0xFFFFFBFA);
const kShrineBackgroundWhite = Colors.white;
自定义调色板
此颜色主题是由设计师使用自定义颜色创建的(如下图所示)。它包含从 Shrine 品牌中选取的颜色,并且已经应用于 Material Theme Editor,该编辑器对其进行了扩展以创建更完整的调色板。(这些颜色并非来自 2014 Material 调色板。)
Material Theme Editor 将它们组织为带有数字标签的暗色调,包括标签 50、100、200......一直到 900,每种颜色对应一个标签。Shrine 只使用粉红色色板的暗色调 50、100 和 300 以及棕色色板的 900。
微件的每个颜色参数都会映射到这些方案其中的一种颜色。例如,文本字段在主动接收输入时,其装饰的颜色应为主题的主色。如果该颜色不易辨认(即很难从背景中进行 区分),请改用 PrimaryVariant。
这些变体是为《2014 Material 指南》创建的,在当前指南和 MDC-Flutter 中仍有提供。要通过代码访问它们,只需调用基色,然后调用暗色调(通常是 100 的倍数)。例如,Pink 400 可通过命令 Colors.pink[400]
检索。
这些调色板完全可以用于您的设计和代码。如果您已经拥有特定于品牌的颜色,那么可以使用调色板生成工具或 Material Theme Editor 来生成自己的和谐调色板。
现在我们有了要使用的颜色,可以将着手它们应用于界面。为此,我们将在微件层次结构的顶部,设置应用于 MaterialApp 实例的 ThemeData 微件的值。
自定义 ThemeData.light()
Flutter 包含一些内置主题。浅色主题就是其中之一。我们将复制浅色主题并更改相应的值,以根据我们的应用对其进行自定义,而不是从头开始构建 ThemeData 微件。
让我们在 app.dart.
中导入 colors.dart
import 'colors.dart';
然后将以下内容添加到 app.dart 中 ShrineApp 类的范围之外:
// TODO: Build a Shrine Theme (103)
final ThemeData _kShrineTheme = _buildShrineTheme();
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light();
return base.copyWith(
accentColor: kShrineBrown900,
primaryColor: kShrinePink100,
buttonTheme: base.buttonTheme.copyWith(
buttonColor: kShrinePink100,
colorScheme: base.colorScheme.copyWith(
secondary: kShrineBrown900,
),
),
buttonBarTheme: base.buttonBarTheme.copyWith(
buttonTextTheme: ButtonTextTheme.accent,
),
scaffoldBackgroundColor: kShrineBackgroundWhite,
cardColor: kShrineBackgroundWhite,
textSelectionColor: kShrinePink100,
errorColor: kShrineErrorRed,
// TODO: Add the text themes (103)
// TODO: Add the icon themes (103)
// TODO: Decorate the inputs (103)
);
}
现在,将 ShrineApp 的 build()
函数(在 MaterialApp 微件中)末尾的 theme:
设为我们的新主题:
// TODO: Add a theme (103)
theme: _kShrineTheme, // New code
点击"Play"(播放)按钮。现在,您的登录屏幕应如下所示:
Android | iOS |
您的主屏幕应如下所示:
Android | iOS |
5. 修改字体和标签样式
除颜色变化外,设计师还为我们提供了要使用的特定字体。Flutter 的 ThemeData 包含 3 个文本主题。每个文本主题都是一些文本样式的集合,例如"大标题"和"标题"。我们将在应用中使用几种样式,并更改一些值。
自定义文本主题
要将字体导入项目,必须先将它们添加到 pubspec.yaml 文件中。
在 pubspec.yaml 中,紧随 flutter:
标记之后添加以下内容:
# TODO: Insert Fonts (103)
fonts:
- family: Rubik
fonts:
- asset: fonts/Rubik-Regular.ttf
- asset: fonts/Rubik-Medium.ttf
weight: 500
现在,您便可以访问和使用 Rubik 字体。
pubspec 文件问题排查
如果您直接剪切并黏贴上述声明,那么在运行 pub get 时可能会发生错误。如果发生错误,请先删除头部空格,然后使用 2 个空格的缩进格式进行替换。(
fonts:
之前两个空格,
family: Rubik
之前四个空格,以此类推。)
如果您看到 Mapping values are not allowed here(此处不允许映射值),请检查有问题的行的缩进格式,及其上方的行的缩进格式。
在 login.dart
中,更改 Column()
中的以下内容:
Column(
children: <Widget>[
Image.asset('assets/diamond.png'),
SizedBox(height: 16.0),
Text(
'SHRINE',
style: Theme.of(context).textTheme.headline5,
),
],
)
在 app.dart
中的 _buildShrineTheme()
后面添加以下内容:
// TODO: Build a Shrine Text Theme (103)
TextTheme _buildShrineTextTheme(TextTheme base) {
return base.copyWith(
headline5: base.headline5.copyWith(
fontWeight: FontWeight.w500,
),
headline6: base.headline6.copyWith(
fontSize: 18.0
),
caption: base.caption.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
bodyText1: base.bodyText1.copyWith(
fontWeight: FontWeight.w500,
fontSize: 16.0,
),
).apply(
fontFamily: 'Rubik',
displayColor: kShrineBrown900,
bodyColor: kShrineBrown900,
);
}
这一步需要获取 **TextTheme,**并更改大标题、标题和小标题的外观。
以这种方式应用 fontFamily
只会将更改应用到 copyWith()
中指定的字体缩放值(大标题、标题、小标题)。
对于某些字体,我们要设置自定义 fontWeight。FontWeight 微件提供的值都是 100 的倍数,十分方便。在字形中,w500(500 字重)通常代表适中粗细的字体,w400 通常是标准粗细的字体。
使用新的文本主题
将以下主题添加到 _buildShrineTheme
中(errorColor 的后面):
// TODO: Add the text themes (103)
textTheme: _buildShrineTextTheme(base.textTheme),
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
点击"Stop"(停止)按钮,然后点击"Play"(播放)按钮。
登录屏幕和主屏幕中的文本看起来不一样:有些文本使用了 Rubik 字体,而另一些文本则显示为棕色,而不是黑色或白色。
Android | iOS |
请注意,图标仍为白色。这是因为图标有单独的主题。
使用自定义的主图标主题
将以下代码添加到 _buildShrineTheme()
函数中:
// TODO: Add the icon theme (103)
primaryIconTheme: base.iconTheme.copyWith(
color: kShrineBrown900
),
点击"Play"(播放)按钮。
Android | iOS |
棕色图标会出现在应用栏中!
缩小文本
屏幕中的标签有点大。
在 home.dart
中,更改最里面一列的 children:
:
// TODO: Change innermost Column (103)
children: <Widget>[
// TODO: Handle overflowing labels (103)
Text(
product == null ? '' : product.name,
style: theme.textTheme.button,
softWrap: false,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
SizedBox(height: 4.0),
Text(
product == null ? '' : formatter.format(product.price),
style: theme.textTheme.caption,
),
// End new code
],
居中放置文本
我们要使标签居中,并使文本与每个卡片的底部对齐,而不是与每张图像的底部对齐。
将标签移到主轴的末端(底部)并将其更改为居中:
// TODO: Align labels to the bottom and center (103)
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
保存项目。
Android | iOS |
很接近了,但文本还没有在卡片上居中。
更改父列横轴的对齐方式:
// TODO: Center items on the card (103)
crossAxisAlignment: CrossAxisAlignment.center,
保存项目。您的主屏幕现在应如下所示:
Android | iOS |
这样效果好多了。
设置文本字段的主题
您还可以使用 InputDecorationTheme 设置文本字段上的装饰主题。
在 app.dart
的 _buildShrineTheme()
方法中,指定 inputDecorationTheme:
值:
// TODO: Decorate the inputs (103)
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(),
),
现在,文本字段具有 filled
装饰。我们来移除它。移除 filled
并指定 inputDecorationTheme
将会为文本字段提供轮廓样式。
在 login.dart
中,移除 filled: true
值:
// Remove filled: true values (103)
TextField(
controller: _usernameController,
decoration: InputDecoration(
// Removed filled: true
labelText: 'Username',
),
),
SizedBox(height: 12.0),
TextField(
controller: _passwordController,
decoration: InputDecoration(
// Removed filled: true
labelText: 'Password',
),
obscureText: true,
),
点击"Run"(运行)菜单下的 Flutter Hot Restart(Flutter 热重启)按钮(以从头开始重新启动应用)。当"Username"(用户名)字段处于活动状态(您正在其中输入)时,您的登录屏幕应如下所示:
Android | iOS |
在文本字段中输入 - 装饰和悬浮占位符均以主色渲染。但我们很难看到它。对于在颜色对比度不够高的情况下难以区分像素的人来说,这些元素不易辨认。让我们在 inputDecorationTheme:
中指定 focusedBorder:
的值,使其成为设计师在上述颜色主题中为我们提供的 PrimaryVariant,以覆盖文本字段的强调色。
在 app.dart
的 inputDecorationTheme:
下方,指定 focusedBorder:
:
// TODO: Decorate the inputs (103)
inputDecorationTheme: InputDecorationTheme(
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
border: OutlineInputBorder(),
),
接下来,我们将更改 两个 文本字段的 labelStyle
属性,以设计师提供的颜色主题为标签着色。
在 login.dart
中,在两个 TextField
微件 InputDecoration()
下添加 labelStyle:
:
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username',
labelStyle: TextStyle(color: Theme.of(context).accentColor),
),
),
SizedBox(height: 12.0),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
labelStyle: TextStyle(color: Theme.of(context).accentColor),
),
),
为使文本字段被选中和未选中时呈现不同的标签样式,我们希望为每个 TextField
微件设置一个 FocusNode
,以及根据微件是否被选中而动态呈现 labelStyle
的条件。
在 login.dart
中,让我们在 _LoginPageState
类顶部文本字段控制器下方初始化 FocusNodes
和未选中时的标签颜色:
class _LoginPageState extends State<LoginPage> {
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _unfocusedColor = Colors.grey[600];
final _usernameFocusNode = FocusNode();
final _passwordFocusNode = FocusNode();
覆盖 initState
并为 FocusNodes
添加一个监听器:
@override
void initState() {
super.initState();
_usernameFocusNode.addListener(() {
setState(() {
//Redraw so that the username label reflects the focus state
});
});
_passwordFocusNode.addListener(() {
setState(() {
//Redraw so that the password label reflects the focus state
});
});
}
最后,在 TextField
微件内添加 focusNode:
属性 ,并在 InputDecoration()
下为 labelStyle:
添加条件:
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username',
labelStyle: TextStyle(
color: _usernameFocusNode.hasFocus
? Theme.of(context).accentColor
: _unfocusedColor),
),
focusNode: _usernameFocusNode,
),
SizedBox(height: 12.0),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
labelStyle: TextStyle(
color: _passwordFocusNode.hasFocus
? Theme.of(context).accentColor
: _unfocusedColor),
),
focusNode: _passwordFocusNode,
),
点击"Play"(播放)按钮。
Android | iOS |
6. 调整高度
现在,您已经按照与 Shrine 匹配的特定颜色和字体为页面设置了样式,下面让我们看一下展示 Shrine 产品的卡片。现在,这些卡片位于网站导航旁边的白色表面上。
调整卡片高度
在 home.dart
中,将 elevation:
值添加到卡片:
// TODO: Adjust card heights (103)
elevation: 0.0,
保存项目。
Android | iOS |
您已经移除卡片下方的阴影。
让我们在登录屏幕上更改组件的高度进行补充。
更改"NEXT"(下一步)按钮的高度
RaisedButton 的默认高度为 2。我们来增大这个值。
在 login.dart
中,将 elevation:
值添加到 NEXT RaisedButton:
RaisedButton(
child: Text('NEXT'),
elevation: 8.0, // New code
点击"Run"(运行)菜单下的 Flutter Hot Restart(Flutter 热重启)按钮(以从头开始重新启动应用)。现在,您的登录屏幕应如下所示:
Android | iOS |
7. 添加形状
Shrine 具有很酷的几何样式:用八角形或矩形形状定义各种元素。让我们在主屏幕的卡片以及登录屏幕的文本字段和按钮中实现该形状样式。
更改登录屏幕上的文本字段形状
在 app.dart
中,导入特殊切角边框文件:
import 'supplemental/cut_corners_border.dart';
还是在 app.dart
中,向文本字段装饰主题添加带有切角的形状:
// TODO: Decorate the inputs (103)
inputDecorationTheme: InputDecorationTheme(
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrineBrown900,
),
),
border: CutCornersBorder(), // Replace code
),
更改登录屏幕上的按钮形状
在 login.dart
中,向 **CANCEL(取消)**按钮添加斜角矩形边框:
FlatButton(
child: Text('CANCEL'),
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
FlatButton 没有可见的形状,那么我们为什么要添加边框形状呢?因为发生轻触时,波纹动画会绑定到同一个形状。
现在,将相同的形状添加到"NEXT"(下一步)按钮:
RaisedButton(
child: Text('NEXT'),
elevation: 8.0,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(7.0)),
),
要更改所有按钮的形状,我们还可以更新 app.dart
中的 buttonTheme
。这是留给学习者的挑战!
点击"Run"(运行)菜单下的 Flutter Hot Restart(Flutter 热重启)按钮(从头开始重新启动应用):
Android | iOS |
8. 更改布局
接下来,让我们更改布局,以不同的宽高比和大小显示卡片,使每张卡片看起来都与众不同。
用 AsymmetricView 替换 GridView
我们已经写好非对称布局的文件。
在 home.dart
中,将整个文件更改为以下内容:
import 'package:flutter/material.dart';
import 'model/products_repository.dart';
import 'model/product.dart';
import 'supplemental/asymmetric_view.dart';
class HomePage extends StatelessWidget {
// TODO: Add a variable for Category (104)
@override
Widget build(BuildContext context) {
// TODO: Return an AsymmetricView (104)
// TODO: Pass Category variable to AsymmetricView (104)
return Scaffold(
appBar: AppBar(
brightness: Brightness.light,
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
print('Menu button');
},
),
title: Text('SHRINE'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
print('Search button');
},
),
IconButton(
icon: Icon(Icons.tune),
onPressed: () {
print('Filter button');
},
),
],
),
body: AsymmetricView(products: ProductsRepository.loadProducts(Category.all)),
);
}
}
保存项目。
Android | iOS |
现在,产品以类似于编织的模式水平滚动。另外,状态栏文本(顶部的时间和网络状态)现在是黑色的。这是因为我们将 AppBar 的亮度改成了亮 brightness: Brightness.light
9. 尝试其他主题
颜色是有力的品牌表达方式,颜色的微小变化会对用户体验产生重大影响。 为验证这一点,让我们看看在品牌的配色方案完全不同的情况下 Shrine 的外观会如何。
修改颜色
在 colors.dart
中,添加以下内容:
const kShrinePurple = Color(0xFF5D1049);
const kShrineBlack = Color(0xFF000000);
在 app.dart
中,将 _buildShrineTheme()
和 _buildShrineTextTheme
函数更改为以下内容:
ThemeData _buildShrineTheme() {
final ThemeData base = ThemeData.light();
return base.copyWith(
primaryColor: kShrinePurple,
buttonTheme: base.buttonTheme.copyWith(
buttonColor: kShrinePurple,
textTheme: ButtonTextTheme.primary,
colorScheme: ColorScheme.light().copyWith(primary: kShrinePurple)
),
scaffoldBackgroundColor: kShrineSurfaceWhite,
textTheme: _buildShrineTextTheme(base.textTheme),
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
primaryIconTheme: base.iconTheme.copyWith(
color: kShrineSurfaceWhite
),
inputDecorationTheme: InputDecorationTheme(
focusedBorder: CutCornersBorder(
borderSide: BorderSide(
width: 2.0,
color: kShrinePurple,
),
),
border: CutCornersBorder(),
),
);
}
TextTheme _buildShrineTextTheme(TextTheme base) {
return base.copyWith(
headline5: base.headline5.copyWith(
fontWeight: FontWeight.w500,
),
headline6: base.headline6.copyWith(
fontSize: 18.0,
),
caption: base.caption.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14.0,
),
bodyText1: base.bodyText1.copyWith(
fontWeight: FontWeight.w500,
fontSize: 16.0,
),
).apply(
fontFamily: 'Rubik',
);
}
在 login.dart
中,将钻石徽标的颜色涂成黑色:
Image.asset(
'assets/diamond.png',
color: kShrineBlack, // New code
),
在 home.dart
中,将 AppBar
的亮度更改为深色:
brightness: Brightness.dark,
保存项目。现在应该会显示新的主题。
Android | iOS |
Android | iOS |
结果大不相同!在继续学习 104 至前,让我们还原此颜色代码。
10. 总结
现在,您创建了一个与设计师的设计规范相似的应用。
后续步骤
现在,您使用到了以下 MDC 组件:主题、字体、高度和形状。您可以在 MDC-Flutter 库中探索更多组件和子系统。
深入研究 supplemental
目录中的文件,了解如何制作水平滚动的非对称布局网格。
如果您计划的应用设计中包含的元素在 MDC 库中找不到相应的组件,该怎么办?在 MDC-104:Material Design 高级组件中,我们将演示如何使用 MDC 库创建自定义组件,以实现特定外观。