1. 简介
Material Components (MDC) 可帮助开发者实施 Material Design。MDC 由 Google 的工程师和用户体验设计师团队创建,拥有数十个精美而强大的界面组件,可用于 Android、iOS、Web 和 Flutter。material.io/develop |
在 Codelab MDC-103 中,您通过自定义 Material Components (MDC) 的颜色、高度、字体和形状,设置了应用的样式。
Material Design 系统中的组件可用于执行一组预定义的任务,并且具有某些特性,例如按钮。但按钮不只是用户执行操作的一种方式,还是形状、大小和颜色的一种视觉表达,可以让用户知道它是可交互的,轻触或点击将会触发一些行为。
Material Design 指南以设计师的角度来描述组件。具体描述了可用于各个平台的各种基本功能,以及组成每个组件的结构元素。例如,背景包含背图层及其内容、前图层及其内容、运动规则和显示选项。其中每个组件都可针对每个应用的需求、使用情况和内容进行自定义。主要包括传统的视图、控件以及您所处平台 SDK 的功能。
虽然 Material Design 指南命名了很多组件,但并非所有的组件都适合用作可重复使用的代码, 因此您在 MDC 中找不到这些组件。您可以全部使用传统代码自行打造这些体验,为您的应用自定义风格。
您将构建的应用
在本 Codelab 中,您会将 Shrine 应用中的界面更改为称作"背景"的二级显示。背景包括一个 菜单,其中列出了用于过滤非对称网格中显示的产品的可选择类别。在本 Codelab 中,您将使用以下 Flutter 组件:
- 形状
- 动作
- Flutter 微件(在之前的 Codelab 中所使用的)
Android | iOS |
本 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-103?
如您完成了 MDC-103,那么您的代码应该已经可以用于此 Codelab。请跳到以下步骤:添加背景菜单。
从头开始?
起始应用位于 material-components-flutter-codelabs-104-starter_and_103-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 104-starter_and_103-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 设备选择)-> Open iOS Simulator(打开 iOS 模拟器),以在开发机器上启动模拟器。) |
2.启动 Flutter 应用:
|
成功!您应该会在模拟器中看到上一个 Codelab 中的 Shrine 登录页面。
Android | iOS |
4. 添加背景菜单
背景出现在所有其他内容和组件后面。它由两个图层组成:后图层(用于显示操作和过滤器)和前图层(用于显示内容)。您可以使用背景来显示交互式信息和操作,例如导航或内容过滤。
删除主应用栏
HomePage 微件将成为前图层的内容。现在这上面有一个应用栏。我们会将应用栏移到后图层中,而 HomePage 将只包含 AsymmetricView。
在 home.dart
中,更改 build()
函数使其只返回 AsymmetricView:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
添加 Backdrop 微件
创建一个名为 Backdrop 的微件,其中包含 frontLayer
和 backLayer
。
backLayer
包含一个菜单,可用于选择类别来过滤列表 (currentCategory
)。由于我们希望菜单的选择保持不变,因此将 Backdrop 设为有状态的微件。
在 /lib
中添加一个新文件,并命名为 backdrop.dart
:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'model/product.dart';
// TODO: Add velocity constant (104)
class Backdrop extends StatefulWidget {
final Category currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
const Backdrop({
@required this.currentCategory,
@required this.frontLayer,
@required this.backLayer,
@required this.frontTitle,
@required this.backTitle,
}) : assert(currentCategory != null),
assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null);
@override
_BackdropState createState() => _BackdropState();
}
// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)
系统将导入 meta 软件包以将属性标记为 @required
。当构造函数中的属性没有默认值且不能为 null
时,采用这样的最佳做法以提醒自己不要遗漏。请注意,我们在构造函数后面还有用于检查传递到这些字段的值确实不是 null
的断言。
在 Backdrop 类定义下面,添加 _BackdropState 类:
// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO: Add AnimationController widget (104)
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack() {
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
brightness: Brightness.light,
elevation: 0.0,
titleSpacing: 0.0,
// TODO: Replace leading menu icon with IconButton (104)
// TODO: Remove leading property (104)
// TODO: Create title with _BackdropTitle parameter (104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO: Add open login (104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO: Add open login (104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: _buildStack(),
);
}
}
build()
函数像 HomePage 一样会返回一个包含应用栏的 Scaffold。但 Scaffold 的正文是 Stack。Stack 的子项可以重叠。每个子项的大小和位置相对于 Stack 的父项指定。
现在将 Backdrop 实例添加到 ShrineApp。
在 app.dart
中,导入 backdrop.dart
和 model/product.dart
。
import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';
在 app.dart,
中,修改 ShrineApp 的 build()
函数。将 home:
更改为以 HomePage 作为其 frontLayer
的 Backdrop:
// TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
home: Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: Category.all,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
如果点击"Play"(播放)按钮,您应该会看到主页和应用栏:
Android | iOS |
backLayer 在 frontLayer 主页后面的新图层中显示了一个粉红色区域。
您可以使用 Flutter Inspector 来验证 Stack 在 HomePage 后面确实有一个容器。具体应如下所示:
您现在可以调整两个图层的设计和内容。
5. 添加形状
在此步骤中,您将设置前图层的样式,以在左上角添加一个切口。
Material Design 将此类定制视为形状。Material 表面可以是任意形状。形状为表面增加了重点和风格,可用于表达品牌形象。普通矩形可使用弯曲或成角度的角和边以及任意数量的边来自定义。它们可以是对称的,也可以是不规则的。
将形状添加到前图层
从角形的 Shrine 徽标可以联想到 Shrine 应用的形状故事。形状故事是整个应用中使用的公共形状。例如,徽标形状会与应用了形状的登录页面元素相呼应。在此步骤中,您将使用左上角的角形槽设计前图层的样式。
在 backdrop.dart
中,添加新类 _FrontLayer:
// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key key,
this.child,
}) : super(key: key);
final Widget child;
@override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: BeveledRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
Expanded(
child: child,
),
],
),
);
}
}
然后,在 _BackdropState 的 _buildStack()
函数中,将前图层封装到 _FrontLayer:
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
重新加载。
Android | iOS |
我们已为 Shrine 的主要表面提供自定义的形状。由于表面具有高度,用户可以看出,位于上方的白色图层的后方有一些内容。让我们加入动作,以便用户看到背景的后图层。
6. 添加动作
动作可以让应用变得生动有趣。它可以热烈奔放,也可以舒缓轻柔,或介于两者之间。 但请注意,您使用的动作类型应与情景相符。应用于重复性、规律性操作的动作应轻柔,以免分散用户的注意力,或者经常耗费过多时间。但在适当的情景中,比如用户第一次打开应用时, 可以使用更加吸引眼球的动作,并且有些动画也可以帮助用户了解如何使用应用。
为菜单按钮添加呈现动作
在 backdrop.dart
顶部的任何类或函数范围外,添加一个常量来表示我们希望动画出现 的速度:
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
将 AnimationController 微件添加到 _BackdropState,在 initState()
函数中对其进行实 例化,然后在状态的 dispose()
函数中释放该微件:
// TODO: Add AnimationController widget (104)
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO: Add override for didUpdateWidget (104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO: Add functions to get and change front layer visibility (104)
AnimationController 用于协调动画,以及提供 API 来播放、倒放和停止动画。现在我们需要能够使其移动的函数。
添加用于确定和更改前图层可见性的函数:
// TODO: Add functions to get and change front layer visibility (104)
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}
将 backLayer 封装到 ExcludeSemantics 微件。当后图层不可见时,此微件将从语义树中排除 backLayer 的菜单项。
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
更改 _buildStack() 函数以提取 BuildContext 和 BoxConstraints。另外,加入 PositionedTransition 以提取 RelativeRectTween Animation:
// TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO: Create a RelativeRectTween Animation (104)
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO: Wrap backLayer in an ExcludeSemantics widget (104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
// TODO: Add a PositionedTransition (104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
child: widget.frontLayer,
),
),
],
);
}
最后,不要调用用于 Scaffold 正文的 _buildStack 函数,而是返回将 _buildStack 用作其构建器的 LayoutBuilder 微件:
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
我们已将前/后图层栈的构建延迟到使用 LayoutBuilder 进行布局的时间,以便我们可以结合背景的实际整体高度。LayoutBuilder 是一个特殊的微件,其构建器回调提供对大小的约束。
在 build()
函数中,将应用栏中的头部菜单图标变成 IconButton,并在按钮被点按时用它来切换前图层的可见性。
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
重新加载,然后点按模拟器中的菜单按钮。
Android | iOS |
前图层动画(幻灯片)关闭。但如果您向下看,会发现红色错误和溢出错误。这是因为,AsymmetricView 被此动画压缩而变得更小,从而减小了 Column 的空间。最后,Column 在提供的空间内无法布置,从而产生错误。如果我们将 Column 替换为 ListView,列大小在动画播放时应会保持不变。
将产品列封装在 ListView 中
在 supplemental/product_columns.dart
中,将 OneProductCardColumn
中的 Column 替换为 ListView:
class OneProductCardColumn extends StatelessWidget {
OneProductCardColumn({this.product});
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
reverse: true,
children: <Widget>[
SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}
Column 包含 MainAxisAlignment.end
。要从底部开始布置,请标记 reverse: true
。子项的顺序将会颠倒,以抵消这一更改。
重新加载,然后点按菜单按钮。
Android | iOS |
OneProductCardColumn 上的灰色溢出警告消失了!现在我们来修复另一个问题。
在 supplemental/product_columns.dart
中,改变计算 imageAspectRatio
的方式, 并将 TwoProductCardColumn
中的 Column 替换为 ListView:
// TODO: Change imageAspectRatio calculation (104)
double imageAspectRatio = heightOfImages >= 0.0
? constraints.biggest.width / heightOfImages
: 49.0 / 33.0;
// TODO: Replace Column with a ListView (104)
return ListView(
physics: const ClampingScrollPhysics(),
children: <Widget>[
Padding(
padding: EdgeInsetsDirectional.only(start: 28.0),
child: top != null
? ProductCard(
imageAspectRatio: imageAspectRatio,
product: top,
)
: SizedBox(
height: heightOfCards,
),
),
SizedBox(height: spacerHeight),
Padding(
padding: EdgeInsetsDirectional.only(end: 28.0),
child: ProductCard(
imageAspectRatio: imageAspectRatio,
product: bottom,
),
),
],
);
我们还为 imageAspectRatio
添加了一些安全条件。
重新加载。然后点按菜单按钮。
Android | iOS |
不再出现溢出了。
7. 在后图层上添加一个菜单
菜单是可点按的文本项列表,当文本项被轻触时菜单会向监听器发送通知。在这一步中,您将添加一个类别过滤菜单。
添加菜单
将菜单加入前图层,并将交互式按钮加入后图层。
创建名为 lib/category_menu_page.dart
的新文件:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'colors.dart';
import 'model/product.dart';
class CategoryMenuPage extends StatelessWidget {
final Category currentCategory;
final ValueChanged<Category> onCategoryTap;
final List<Category> _categories = Category.values;
const CategoryMenuPage({
Key key,
@required this.currentCategory,
@required this.onCategoryTap,
}) : assert(currentCategory != null),
assert(onCategoryTap != null);
Widget _buildCategory(Category category, BuildContext context) {
final categoryString =
category.toString().replaceAll('Category.', '').toUpperCase();
final ThemeData theme = Theme.of(context);
return GestureDetector(
onTap: () => onCategoryTap(category),
child: category == currentCategory
? Column(
children: <Widget>[
SizedBox(height: 16.0),
Text(
categoryString,
style: theme.textTheme.bodyText1,
textAlign: TextAlign.center,
),
SizedBox(height: 14.0),
Container(
width: 70.0,
height: 2.0,
color: kShrinePink400,
),
],
)
: Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Text(
categoryString,
style: theme.textTheme.bodyText1.copyWith(
color: kShrineBrown900.withAlpha(153)
),
textAlign: TextAlign.center,
),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.only(top: 40.0),
color: kShrinePink100,
child: ListView(
children: _categories
.map((Category c) => _buildCategory(c, context))
.toList()),
),
);
}
}
它是用于封装列的 GestureDetector,其子项是类别的名称。下划线用于表示所选的类别。
在 app.dart
中,将 ShrineApp 微件从无状态转换为有状态。
- 突出显示
ShrineApp.
- 点按 alt (option) + enter。
- 选择"Convert to StatefulWidget"(转换为 StatefulWidget)。
- 将 ShrineAppState 类更改为私有 (_ShrineAppState)。要从 IDE 主菜单执行此操作,请选择 Refactor(重构)> Rename(重命名)。或者,在代码内,您可以突出显示类名称 ShrineAppState,然后右键点击并选择 Refactor(重构)> Rename(重命名)。输入 _ShrineAppState 以将该类设为私有。
在 app.dart
中,添加变量到所选类别的 _ShrineAppState,以及当其被点按时使用的回调函数:
// TODO: Convert ShrineApp to stateful widget (104)
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
然后将后图层更改为 CategoryMenuPage。
在 app.dart
中,导入 CategoryMenuPage:
import 'backdrop.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';
在 build()
函数中,将 backlayer 字段更改为 CategoryMenuPage 和 currentCategory 字段以提取实例变量。
home: Backdrop(
// TODO: Make currentCategory field take _currentCategory (104)
currentCategory: _currentCategory,
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(),
// TODO: Change backLayer field value to CategoryMenuPage (104)
backLayer: CategoryMenuPage(
currentCategory: _currentCategory,
onCategoryTap: _onCategoryTap,
),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
重新加载,然后点按菜单按钮。
Android | iOS |
如果您点按菜单选项,但......没有任何反应。我们来解决这个问题。
在 home.dart
中,为 Category 添加变量并将其传递到 AsymmetricView。
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)
final Category category;
const HomePage({this.category: Category.all});
@override
Widget build(BuildContext context) {
// TODO: Pass Category variable to AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(category));
}
}
在 app.dart
中,为 frontLayer
传递 _currentCategory
。
// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),
重新加载。点按模拟器中的菜单按钮,然后选择一个类别。
Android | iOS |
点按菜单图标以查看产品。它们经过了过滤!
在菜单选择操作后关闭前图层
在 backdrop.dart
中,为 _BackdropState 中的 didUpdateWidget()
函数添加覆盖:
// TODO: Add override for didUpdateWidget() (104)
@override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
热重载项目,然后点按菜单图标并选择一个类别。该菜单应会自动关闭,您应该能看到所选项目的类别。现在您也要将该功能添加到前图层。
切换前图层
在 backdrop.dart
中,将点按时回调添加到背景图层:
class _FrontLayer extends StatelessWidget {
// TODO: Add on-tap callback (104)
const _FrontLayer({
Key key,
this.onTap, // New code
this.child,
}) : super(key: key);
final VoidCallback onTap; // New code
final Widget child;
然后将 GestureDetector 添加到 _FrontLayer 的子项:Column 的子项。
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO: Add a GestureDetector (104)
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
然后在 _buildStack()
函数中的 _BackdropState 上实施新的 onTap
属性:
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO: Implement onTap property on _BackdropState (104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
重新加载,然后点按前图层的顶部。前图层会随着您每次点按其顶部而打开和关闭。
8. 添加品牌图标
品牌图标的表示方法也可扩展到我们熟悉的图标。我们将呈现图标设为自定义,并将其与我们的标题合并,形成独特的品牌外观。
更改菜单按钮图标
Android | iOS |
在 backdrop.dart
中,创建新类 _BackdropTitle。
// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
final Function onPress;
final Widget frontTitle;
final Widget backTitle;
const _BackdropTitle({
Key key,
Listenable listenable,
this.onPress,
@required this.frontTitle,
@required this.backTitle,
}) : assert(frontTitle != null),
assert(backTitle != null),
super(key: key, listenable: listenable);
@override
Widget build(BuildContext context) {
final Animation<double> animation = this.listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.headline6,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
Opacity(
opacity: animation.value,
child: ImageIcon(AssetImage('assets/slanted_menu.png')),
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(1.0, 0.0),
).evaluate(animation),
child: ImageIcon(AssetImage('assets/diamond.png')),
)]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
_BackdropTitle
是一个自定义微件,用于替换 AppBar
微件 title
参数的纯 Text
微件。其拥有动画菜单图标,并且会在前标题与后标题之间进行带有动画的转换。动画菜单图标将使用新的资产。对新 slanted_menu.png
的引用必须添加到 pubspec.yaml
中。
assets:
- assets/diamond.png
# TODO: Add slanted menu asset (104)
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
移除 AppBar
构建器中的 leading
属性。为使自定义品牌图标能够在原来 leading
微件的位置渲染,必须执行上述的移除操作。品牌图标的动画 listenable
和 onPress
处理程序会传递到 _BackdropTitle
。frontTitle
和 backTitle
也会被传递,这样它们就可以在背景标题内渲染。AppBar
的 title
参数应如下所示:
// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
品牌图标在 BackdropTitle 中创建。它包含动画图标的 Stack
:一个斜菜单和一个菱形,都封装在 IconButton
中,以便其可以被点按。IconButton
随后会被封装在 SizedBox
中,以便为水平图标动作腾出空间。
Flutter 的"一切都是微件"架构允许更改默认 AppBar
的布局,而无需创建全新的自定义 AppBar
微件。title
参数(最初是一个 Text
微件)可替换为更复杂的 _BackdropTitle
。由于 _BackdropTitle
也包含自定义图标,因此它会代替 leading
属性,后者现在可以省略。这种简单的微件替代无需更改任何其他参数(例如操作图标),且微件仍可以继续独立运行。
将快捷方式添加回登录屏幕
在 backdrop.dart,
中,将快捷方式从应用栏中的尾部图标添加回登录屏幕:更改图标的语义标签,以反映它们的新用途。
// TODO: Add shortcut to login screen from trailing icons (104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'login', // New code
),
onPressed: () {
// TODO: Add open login (104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
如果您尝试重新加载,则会收到错误。导入 login.dart
以修复错误:
import 'login.dart';
重新加载应用,然后点按搜索或微调按钮以返回登录屏幕。
9. 总结
在学习这四个 Codelab 的过程中,您已经了解如何使用 Material Components 打造独特、 优雅的用户体验,以表现品牌的个性和风格。
后续步骤
本 Codelab(即 MDC-104)是此 Codelab 系列的最后一个。您可以访问 Flutter 微件目录探索 MDC-Flutter 中的更多组件。
如果想更进一步,可尝试将品牌图标替换为 AnimatedIcon,以在背景设为可见时切换播放两个图标的动画。