MDC (Material Components) 帮助开发者实现 Material Design。

MDC 由谷歌的工程师和交互团队联合呈现,其具备数种精美的 UI 组件,适用于 Android、iOS、Web 和 Flutter。

查看详细:material.io/develop

相比较之前,你现在可以使用 MDC(Material Design Component)来让你的应用具有与众不同的风格。Material Design 最新的扩充内容让设计师和开发者能够更灵活地对他们产品的品牌进行展示。

在 MDC-101 和 MDC-102 的 Codelab 中,你使用了 Material 组件(MDC)来创建了一个名为 Shrine 的应用,它是一个在线销售衣服和家用产品的电商应用。启动这个应用就能看到一个登录界面,登录成功之后会引导用户进入首页查看琳琅满目的产品。

本次你将做出什么

在本期 Codelab 中,你将继续对 Shrine 应用进行优化,包括:

我们最终会构建一个卖衣服和家居的电子商务应用 - SHRINE,总共通过四个 Codelabs 搞定它,本 Codelab 是其中的第二个,相关的 Codelab 如下:

在 MDC Flutter 教程 4 结束时,你将有机会做出一个这样的应用:

本期 Codelab 中使用到的组件和子系统

你可能需要准备

在 iOS 上运行 Flutter 应用:

在 Android 上运行 Flutter 应用:

如何定位你的 Flutter 开发水平?

初学者 中级水平 专业

你需要安装两部分来完成本次实验,Flutter 的 SDK编辑器(editor),这个 codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。

你可以通过如下任何设备完成本 codelab:

从 MDC 102 继续?

如果你已经完成了 MDC Flutter 教程 2:Material 组件结构和布局(MDC 102),你可以跳过这一节,直接从下一节继续......

从本节开始?

下载 Codelab 起步应用

点击下载

起步应用的路径在 material-components-flutter-Codelabs-103-starter_and_102-complete/mdc_100_series 文件夹内。

...或者直接从 Github 上进行 clone

使用以下命令从 Github 上 clone 这个 Codelab 项目:

git clone https://github.com/material-components/material-components-flutter-Codelabs.git
cd material-components-flutter-Codelabs
git checkout 103-starter_and_102-complete

设置你的工程

如下设置工程的方法是使用 Android Studio 做演示的。

创建新的工程

1. 在你的命令行工具里,切换到这个文件夹 material-components-flutter-codelabs

2. 执行命令 flutter create mdc_100_series

打开你的工程

1. 打开 Android Studio

2. 当你看到欢迎界面时候,选择打开一个已存在的工程(Open an existing Android Studio project.)

3. 打开这个文件夹 material-components-flutter-codelabs/mdc_100_series

如果此时提示一些错误请忽略,等第一次成功编译之后应该会好一些。

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

5. 如果提示"升级平台和插件"或要配置 FlutterRunConfigurationType,请重启 Android Studio。

运行起步应用

下面我们以在 Android 模拟器或真机上为例做如下的步骤示范,当然如果你装有 Xcode,你也可以在 iOS 模拟器或真机上进行尝试。

1. 选择设备或模拟器

如果 Android 模拟器还没有运行起来,请选择 Tools -> Android -> AVD Manager 先进行 创建并启动模拟器 的操作。如果 AVD 里已经有创建好的模拟器,你可以直接在 IntelliJ 的设备选择列表里启动模拟器,如下一步所示。

(对于 iOS 模拟器,如果你还没有启动,请在你的电脑上选择 Flutter Device Selection -> Open iOS Simulator 来启动。)

2. 运行你的 Flutter 应用:

  • 在你的编辑器上方, Flutter 设备下拉菜单里,选择一款设备(比如选择 iPhone SE 或者某个 Android SDK 编译版本的设备)。
  • 点击 Play 按钮 ()。

你成功啦!你将从之前的 Codelab 中运行代码,就可以在模拟器上看到 Shrine 的登录界面了。

点击 "Next" 进入到应用的首页。

Shrine 已经拥有了一套配色方案,设计师希望这个配色方案贯彻在整个 Shrine 应用中。

让我们开始吧,先在项目中引入这些颜色。

创建 colors.dart

lib 文件夹下创建名为 colors.dart 的文件,然后在其中引入 Material 组件,并加入一些表示颜色的常量。

import 'package:flutter/material.dart';

const kShrinePink50 = const Color(0xFFFEEAE6);
const kShrinePink100 = const Color(0xFFFEDBD0);
const kShrinePink300 = const Color(0xFFFBB8AC);
const kShrinePink400 = const Color(0xFFEAA4A4);

const kShrineBrown900 = const Color(0xFF442B2D);

const kShrineErrorRed = const Color(0xFFC5032B);

const kShrineSurfaceWhite = const Color(0xFFFFFBFA);
const kShrineBackgroundWhite = Colors.white;

定制色板

一位设计师设计了这份色彩主题(配色表),参见下面的图片。它包含了符合 Shrine 品牌的颜色,然后加入到 Material 主题编辑器里,最后生成了这份调色板(这些颜色并非是 2014 Material 色板)。

Material 主题编辑器会把这些图片加上明亮度数字标签,包含从 50, 100, 200 到 900,Shrine 将会使用 50, 100 和 300 明亮度的粉色,以及 900 明亮度的棕色。

Flutter 里的每个 widget 的取色都会参照这份配色。比如,文本字段在主动接收输入时的颜色应该是这份配色表里的 Primary 这个颜色,如果那个颜色不可用,比如跟背景颜色有冲突,则应该使用 PrimaryVariant 这个颜色替代。

Widget 中的每一个表示颜色的参数都代表着配色方案中的一个颜色。举个例子,对于一个文本输入框来说,当它接收文本输入并处于选中状态之后,它的颜色应该是主题中的主色。如果这个颜色并不能获取到,那就使用 PrimaryVariant 来代替。

现在我们有了想使用的颜色,紧接着就可以把它们应用到 UI 当中。我们将通过设置 ThemeData widget 的值来将其应用于 widget 继承链顶部的 MaterialApp 实例上。

自定义 ThemeData.light()

Flutter 有一些内置的主题,light theme 就是其中之一。相比从无到有来创建一个 ThemeData widget,我们倾向于直接使用 light theme 然后替换其中的一些值来达到自定义应用主题的效果。

先让我们引入 colors.dart

import 'colors.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,
    buttonColor: kShrinePink100,
    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 widget 中)方法最后设置 theme 来作为我们的新主题。

// TODO: Add a theme (103)
return MaterialApp(
  title: 'Shrine',
  // TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
  home: HomePage(),
  // TODO: Make currentCategory field take _currentCategory (104)
  // TODO: Pass _currentCategory for frontLayer (104)
  // TODO: Change backLayer field value to CategoryMenuPage (104)
  initialRoute: '/login',
  onGenerateRoute: _getRoute,
  theme: _kShrineTheme, // New code
);

点击 Play 按钮,你的登录界面就会变成下面的样子:

首页也发生了变化:

除了颜色变化之外,设计师们还为我们提供了特定的文本样式。Flutter 的 ThemeData 包含了 3 个文本主题,每个主题都是一组文本样式的集合,比如 "headline" 和 "title"。我们将为我们的应用使用几种样式并更改一些其中的值来达到定制的效果。

定制文本主题

通过在 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 后,你可能会遇到一些报错。你可以通过移除每一行之前的空格,然后用两个空格的缩进来代替。(在

fonts: 

之前增加两个空格,

family: Rubik

等别的地方增加四个空格)。

如果你输入的值不合法,请检查出现问题行的缩进和其上方行的缩进。

app.dart 文件中,在 _buildShrineTheme()之后增加下面的代码:

// TODO: Build a Shrine Text Theme (103)
TextTheme _buildShrineTextTheme(TextTheme base) {
  return base.copyWith(
    headline: base.headline.copyWith(
      fontWeight: FontWeight.w500,
    ),
    title: base.title.copyWith(
        fontSize: 18.0
    ),
    caption: base.caption.copyWith(
      fontWeight: FontWeight.w400,
      fontSize: 14.0,
    ),
  ).apply(
    fontFamily: 'Rubik',
    displayColor: kShrineBrown900,
    bodyColor: kShrineBrown900,
  );
}

这将创建一个 TextTheme,并更改其标题和标题的外观。

之后通过 apply() 的方式设置 fontFamily,将只会影响调用过 copyWith() 的属性(这里包括 headline,title,caption)。

对于某些字体来说,我们将设置一些自定义的字重。FontWeight widget 提供了非常便捷的方法,w500(字重为 500)通常代表中等加粗,而 w400 则代表正常显示的字体。

使用新的文本主题

_buildShrineTheme 里 errorColor 后面添加如下代码:

// TODO: Add the text themes (103)

textTheme: _buildShrineTextTheme(base.textTheme),
primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),

停止并重新运行项目。

在登录和主页中的文本是不同的,有些是用了 Rubit 字体,有些文本是棕色的而不是白色或者黑色。

你可以留意到这些图标还是白色的,这是因为对于图标来说有另外一套主题。

使用自定义的主图标主题

_buildShrineTheme() 函数中添加以下代码:

    // TODO: Add the icon theme (103)
    primaryIconTheme: base.iconTheme.copyWith(
      color: kShrineBrown900
    ),

点击 play 按钮运行项目:

你将会看到在导航栏上的图标变成棕色的了。

缩小文本

目前标签字体看起来有点太大了。

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,

保存项目。

快接近我们想要的效果了,但是文本对于卡片来说仍然没有居中。

更改父列的横轴对齐方式:

// TODO: Center items on the card (103)

    crossAxisAlignment: CrossAxisAlignment.center,

保存,你的首页就会成为以下这样:

这看起来好多了。

更改文本输入框的主题

你也可以使用 InputDecorationTheme 来更改文本输入框的主题。

app.dart 文件中的 _buildShrineTheme() 方法,指定 inputDecorationTheme 的值。

// TODO: Decorate the inputs (103)

inputDecorationTheme: InputDecorationTheme(
  border: OutlineInputBorder(),
),

现在,文本输入框有一个 filled 的属性值,我们将其移除。

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,
),

点击 Stop 按钮,然后点击 Play 按钮(目的是重启应用)。你的登录界面将会变成下图这样,用户名输入栏在选中时将会变换样式。

我们在文本框中输入一些字符,我们却很难分辨出来。人们很难区分出没有足够色彩对比的内容。(有关详细信息,请参阅 Color article 一文中的 "Accessible colors" 。)让我们额外创建一个类,将 widget 的 Accent 颜色值改为设计师给我们的颜色。

login.dart 文件中,在任意类的作用域之外添加如下代码:

// TODO: Add AccentColorOverride (103)
class AccentColorOverride extends StatelessWidget {
  const AccentColorOverride({Key key, this.color, this.child})
      : super(key: key);

  final Color color;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Theme(
      child: child,
      data: Theme.of(context).copyWith(accentColor: color),
    );
  }
}

接下来,你将为文本输入框添加 AccentColorOverride 方法

在 login.dart 中引入颜色:

import 'colors.dart';

将 Username 的文本输入框嵌套在新的 widget 中:

// TODO: Wrap Username with AccentColorOverride (103)
// [Name]
AccentColorOverride(
  color: kShrineBrown900,
  child: TextField(
    controller: _usernameController,
    decoration: InputDecoration(
      labelText: 'Username',
    ),
  ),
),

同样,将 Password 输入框也嵌套进去:

// TODO: Wrap Password with AccentColorOverride (103)
// [Password]
AccentColorOverride(
  color: kShrineBrown900,
  child: TextField(
    controller: _passwordController,
    decoration: InputDecoration(
      labelText: 'Password',
    ),
  ),
),

点击 Play 按钮。

现在你为 Shrine 应用添加了一套适合它的颜色和字体样式,接下来我们来看一下 Shrine 的卡片,这些卡片位于导航栏下方的白色区域。

调整卡片的 elevation

home.dart 文件中,为 Cards 添加 elevations:

// TODO: Adjust card heights (103)

    elevation: 0.0,

保存。

现在你已经移除了卡片底下的阴影。

让我们更改登录页面上组件的阴影。

更改 NEXT 按钮的 elevation

RaisedButtongs 默认的 elevation 值是 2,我们把按钮变得高一点。

login.dart 文件中,更改 NEXT RaisedButton 的 elevation:

RaisedButton(
  child: Text('NEXT'),
  elevation: 8.0, // New code

点击 Stop 按钮,再点击 Play 按钮,你的登录界面变得如下图这样:


Shrine 具有酷炫的几何风格,它定义了八角形和矩形的元素。 让我们为主屏幕上的卡片,登录页的文本输入框、按钮添加这种形状的样式。

更改登录界面的文本输入框

app.dart 文件中导入一个特殊的切角边框文件:

import 'supplemental/cut_corners_border.dart';

依旧在 app.dart 中,为文本框主题添加带有切角形状的样式:

// TODO: Decorate the inputs (103)
inputDecorationTheme: InputDecorationTheme(
  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)),
  ),

点击 Stop 按钮,然后点击 Play 按钮:

接下来,我们来更改布局以显示不同宽高比和不同大小的卡片,让每张卡片看起来都与众不同。

用 AsymmetricView 来替代 GrideView

我们之前已经用代码实现过不对称的布局效果。

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)),
    );
  }
}

保存。

现在,产品以一种编织图案的方式展示,并可以水平滚动。此外,状态栏主题色(顶部的时间和网络)现在变成了黑色的。这是因为我们更改了 AppBar 的 brightness 属性为 Brightness.light

颜色是进行品牌展示的有效方式,颜色的微小变化会对产品的用户体验产生很大影响。 为了测试这一点,让我们看看如果配色方案完全不同,Shrine 会是什么样子。

更改颜色

colors.dart 文件增加下面的代码:

const kShrineAltDarkGrey = const Color(0xFF414149);
const kShrineAltYellow = const Color(0xFFFFCF44);

app.dart 文件中,将 _buildShrineTheme()_buildShrineTextTheme 方法更改为如下代码:

ThemeData _buildShrineTheme() {
  final ThemeData base = ThemeData.dark();
  return base.copyWith(
    accentColor: kShrineAltDarkGrey,
    primaryColor: kShrineAltDarkGrey,
    buttonColor: kShrineAltYellow,
    scaffoldBackgroundColor: kShrineAltDarkGrey,
    cardColor: kShrineAltDarkGrey,
    textSelectionColor: kShrinePink100,
    errorColor: kShrineErrorRed,
    textTheme: _buildShrineTextTheme(base.textTheme),
    primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme),
    accentTextTheme: _buildShrineTextTheme(base.accentTextTheme),
    primaryIconTheme: base.iconTheme.copyWith(
      color: kShrineAltYellow
    ),
    inputDecorationTheme: InputDecorationTheme(
      border: CutCornersBorder(),
    ),
  );
}

TextTheme _buildShrineTextTheme(TextTheme base) {
  return base.copyWith(
    headline: base.headline.copyWith(
      fontWeight: FontWeight.w500,
    ),
    title: base.title.copyWith(
      fontSize: 18.0
    ),
    caption: base.caption.copyWith(
      fontWeight: FontWeight.w400,
      fontSize: 14.0,
    ),
  ).apply(
    fontFamily: 'Rubik',
    displayColor: kShrineSurfaceWhite,
    bodyColor: kShrineSurfaceWhite,
  );
}

login.dart 文件中,将 logo 的颜色更改为钻石白:

Image.asset(
  'assets/diamond.png',
  color: kShrineBackgroundWhite, // New code
),

同样在 login.dart,将文本输入框的选中颜色更改为黄色:

AccentColorOverride(
  color: kShrineAltYellow, // Changed code
  child: TextField(
    controller: _usernameController,
    decoration: InputDecoration(
      labelText: 'Username',
    ),
  ),
),
SizedBox(height: 12.0),
AccentColorOverride(
  color: kShrineAltYellow, // Changed code
  child: TextField(
    controller: _passwordController,
    decoration: const InputDecoration(
      labelText: 'Password',
    ),
  ),
),

home.dart 中将亮度更改为 dark:

brightness: Brightness.dark,

保存,然后新的主题就如下所示了:

这个看起来跟之前完全不一样,我们先把这些颜色的代码还原,以便学习接下来的 104 课程。

下载 MDC-104 起步代码

到目前为止,你已经创建了一个遵循设计规范的应用程序。

下一步

你现在使用了以下 MDC 组件:theme,typography,elevation 和 shape。 你可以在 MDC-Flutter 库中探索更多的组件和子系统。

你可以查看 supplemental 内的文件,了解我们如何制作水平滚动的非对称布局网格的效果。

如果你要做的应用设计中没有包含于 MDC 库的元素,该怎么办呢? 在 MDC-104:Material Design 进阶中,我们将展示如何使用 MDC 库创建自定义组件以实现特定外观。

我能够用合理的时间和精力完成这个代码库

非常赞同 赞同 一般 不赞同 非常不赞同

我希望将来继续使用 Material Components

非常赞同 赞同 一般 不赞同 非常不赞同