MDC (Material Components) 帮助开发者实现 Material Design。 MDC 由谷歌的工程师和交互团队联合呈现,其具备数种精美的 UI 组件,适用于 Android、iOS、Web 和 Flutter。 查看详细:material.io/develop |
在 codelab MDC-103 里,你已经完成了具有 Material 组件(MDC)风格的颜色、阴影、文本样式、几何形状的定制。
Material Design 体系里的组件都会有预设的职责和明确的特征。例如按钮,它不仅仅只有一种表现形式,在视觉上,它会用形状、大小、颜色等让用户知道这个按钮是可操作的,当手指放上去或者点击时是会有事情会发生的。
Material Design 手册的制定是从设计师的角度出发来描述组件的样子。它包括了不同平台功能的一些基础元素,而后构成了每个组件。比如,一个背景板(backdrop)包含着背景内容、前景内容、运动轨迹和显示行选项。你可以根据不同应用不同平台不同的需求来定制这些组件,这些内容大部分都来自于不同平台 SDK 里的传统视图,控件和功能。
虽然 Material Design 手册已经帮你定义和命名了很多组件,但不要受限于这些已有的内容,你也可以创建一些新的设计或组件。
在本期 Codelab 中,你将会把 Shrine 应用的 UI 更改为一个叫做 "backdrop" 的两级演示文稿。这个 backdrop 包括一个菜单,该菜单列出了用于过滤产品类别来对列表中的产品进行筛选。在本期 Codelab 中,你将使用以下 Flutter 组件:
我们最终会构建一个卖衣服和家居的电子商务应用 - SHRINE,总共通过四个 Codelabs 搞定它,本 Codelab 是最后一个,相关的 Codelabs 如下: |
在这个 Codelab 中用到的 MDC-Flutter 组件
你需要安装两部分来完成本次实验,Flutter 的 SDK 和编辑器(editor),这个 codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。
你可以通过如下任何设备完成本 codelab:
如果你已经完成了 MDC Flutter 教程 3:Material 组件主题、形状、阴影和类型(MDC 103),你可以跳过这一节,直接从下一节继续......
起步应用的路径在 material-components-flutter-Codelabs-104-starter_and_103-complete/mdc_100_series 文件夹内。
使用以下命令从 Github 上 clone 这个 Codelab 项目:
git clone https://github.com/material-components/material-components-flutter-Codelabs.git cd material-components-flutter-Codelabs git checkout 104-starter_and_103-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 应用:
|
你成功啦!你将从之前的 Codelab 中运行代码,就可以在模拟器上看到 Shrine 的登录界面了。
所有的内容和组件背后都有 backdrop,它包括了后层(back layer)(用来显示 actions 和 filters)以及前层(front layer)(用来显示内容)。你可以使用 backdrop 来显示交互信息和操作,比如导航栏和内容过滤器。
HomePage widget 将是前层的内容。现在它有一个应用栏,我将会把应用栏移到后层,让 HomePage 只包含 AsymmetricView。
在 home.dart
文件中更改 build()
函数让它值返回 AsymmetricView
:
// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
创建一个名为 Backdrop 的 widget,包含 frontLayer
和 backLayer
。
其中 backLayer
包含了可以允许你选择某个目录来对列表(currentCategory
)进行筛选的菜单。由于我们希望菜单内的数据保持不变,因此我们将 Backdrop 作为 stateful widget。
在 /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 时的最佳做法,切记这一标记。 请注意,可在构造函数之后添加了 asserts 语句,用来检查传递过来的值不会为 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>[
widget.backLayer,
widget.frontLayer,
],
);
}
@override
Widget build(BuildContext context) {
var appBar = AppBar(
brightness: Brightness.light,
elevation: 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 的子项是可以进行重叠的,每个子项的大小和位置都是相对于它们的父级指定的。
现在为 ShrineApp 增加 Backdrop 实例。
在 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
的值更改为 Backdrop
,其内部 frontLayer
的值为 HomePage:
// 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 按钮,你将会看到我们的主界面同应用栏一起展示了出来。
backLayer 在 frontLayer 主页后面的新图层中显示粉红色区域。你可以使用 Flutter Inspector 来验证 Stack 中的 HomePage 之后是否有一个 Container,它跟这个应该是类似的:
你现在可以调整两个图层的样式和内容。
在这一步中,你将为 front layer 设置样式,在其左上角添加一个剪裁效果。
Material Design 将此类的定制称为 shape。 现实世界中的材料表面可以具有任意形状。 shape 使表面看起来更突出,可用于设计概念的表达。普通的矩形可以定制成具有弯曲或有角度的角和边缘的形状,也可以定位成具有任意数量边的形状。 它们可以是对称的或不规则的。
在 front layer 中添加 shape
具有一定角度的 logo 让整个 Shrine 应用看上去有棱有角,也会体现出整个应用的设计语言。 设计语言是贯穿于整个应用中的。例如,logo 的形状在应用的登录页面元素中会体现出来。 在此步骤中,你将为 front layer 更改为具有倾斜切割风格的样式。
在 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
方法传入 front layer
:
Widget _buildStack() {
// TODO: Create a RelativeRectTween Animation (104)
return Stack(
key: _backdropKey,
children: <Widget>[
widget.backLayer,
// TODO: Add a PositionedTransition (104)
// TODO: Wrap front layer in _FrontLayer (104)
_FrontLayer(child: widget.frontLayer),
],
);
}
刷新。
我们已为 Shrine 的主界面创建出了一个自定义的图形。因为表面阴影层次的关系,白色层下面还有一层。接下来我们增加一点移动效果让用户能够看到背景层。
Motion 是一种让应用看起来仿佛展示于真实世界的方法。它体现出来的效果可以是大而戏剧性的,也可以是很微妙的,也可以介于两者之间。 但请记住,你使用的 motion 类型应该根据使用场景进行选择。应用于重复、常规动作的 motion 效果应该小而精细,这样的动作不会分散用户的注意力或占用太多时间。 但也有特殊情况,例如用户第一次打开应用时,可能要有一些引人注目的效果来吸引用户,而某些 motion 效果可以起到教育用户如何使用的目的。
在 backdrop.dart
文件中任意一个类的作用域之外,添加一个常量,代表我们想添加的动画的表现速度。
// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;
在 _BackdropState 中添加 AnimationController widget,然后在 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 接口,现在我们需要使用它实现移动的效果。
添加可更改 front layer 可见性的函数:
// 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 widget 之中。当 back layer 不可见时,这个 widget 会将 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>[
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 widget。
return Scaffold(
appBar: appBar,
// TODO: Return a LayoutBuilder widget (104)
body: LayoutBuilder(builder: _buildStack),
);
我们使用 LayoutBuilder 让 front/back layer 在布局时候才进行构建,这样可以高效计算出 backdrop 的实际高度。LayerBuilder 是一个特殊的 widget,其构建函数的回调会提供大小的限制。
在 build()
函数中,将应用栏中的菜单图标转换为 IconButton ,并在点击按钮时使用它来切换 front layer 的可见性。
// TODO: Replace leading menu icon with IconButton (104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
刷新,然后点击模拟器中的菜单按钮。
front layer 会有一个滑下去的动画效果。但如果往下看,会发现有个红色的溢出错误。这是因为 AsymmetricView 被这个动画挤压并变小,这导致给 Columns 提供了更少的空间。最终,Columns 不能用给定的空间自行进行排列,因为这样会排列出错。如果我们使用 ListViews 来代替 Columns,那每一列的大小在动画的显示过程中都会保持不变。
在 supplemental/product_columns.dart
文件中,用 ListView
代替 Column
中的 OneProductCardColumn
:
class OneProductCardColumn extends StatelessWidget {
OneProductCardColumn({this.product});
final Product product;
@override
Widget build(BuildContext context) {
// TODO: Replace Column with a ListView (104)
return ListView(
reverse: true,
children: <Widget>[
SizedBox(
height: 40.0,
),
ProductCard(
product: product,
),
],
);
}
}
这个 Column 包含了 MainAxisAlignment.end
。为了从底部进行排列,将 reverse
的值改为 true
。更改之后,子视图的渲染顺序是相反的。
刷新,然后点击菜单按钮。
OneProductCardColumn 上的灰色溢出警告消失了! 现在让我们修复另一个。
在 supplemental/product_columns.dart
文件中,更改 imageAspectRatio
的计算方式,然后用 ListView
来替换 TwoProductCardColumn
:
// TODO: Change imageAspectRatio calculation (104)
double imageAspectRatio =
(heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages)
? constraints.biggest.width / heightOfImages
: 33 / 49;
// TODO: Replace Column with a ListView (104)
return ListView(
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
里添加了一些安全检查的措施。
刷新,然后点击菜单按钮。
不会再有溢出的现象出现了。
菜单是可触发文本选项展示的列表,文本被选中时触发相关的回调监听。在这一步,你会实现一个类别过滤的菜单。
在 front layer 中添加菜单,并在 back layer 中添加一个可交互的按钮。
创建一个新文件,路径为 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.body2,
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.body2.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,包含一个 Column,内容是要展示类别的名称。用下划线指示当前所选类别。
在 app.dart
文件中,将 ShrineApp widget 从一个 stateless widget 转变为 stateful widget。
ShrineApp
;在 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;
});
}
然后将 backLayer 更改为 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'),
),
刷新,然后点击菜单按钮。
如果你点击菜单选项,什么事都不会发生... 接下来我们来修复这个问题。
在 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),
刷新,点击模拟器中的菜单按钮,然后选择一个类别。
点击菜单图标查看销售的产品,然后进行筛选吧!
在 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);
}
}
热重载之后点击菜单栏,选择一个分类。在你看到所选择的项目类别之后菜单应该自动关闭,现在将这个功能添加到 front layer 中。
在 backdrop.dart
文件中,为 backdrop layer
增加一个点击触发的回调。
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;
在 _FrontLayer's child: Column 的 children 字段里添加一个 GestureDetector :
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,
),
),
刷新,点击 front layer 上方,这个图层会打开,在你再次点击 front layer 上方时,它会自动关闭。
一款品牌图标应做到被大众所熟悉。让我们来展示出一个独一无二的品牌图标,并将把它和应用标题合起来吧。
在 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.title,
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
是一个自定义的 widget,它将替换 AppBar
widget 的 title
参数中的文本。它提供了有着动画效果的菜单图标,和可以在不同层级的标题之间切换的过渡动画。这个菜单图标将会使用一个新的资源,这个资源将通过在 pubsepc.yaml
文件中添加 slanted_menu.png
来进行引入。
assets:
- assets/diamond.png
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
删除 AppBar
构建方法中的 leading
属性,需要删除自定义的品牌图标才能在原始的 widget 中进行显示。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的"一切都是 widget "的设计理念允许更改默认 AppBar
的布局,而无需从零自定义 AppBar
widget。 title
参数最初是一个 Text
widget,可以用更复杂的 _BackdropTitle
替换。 由于 _BackdropTitle
还包含自定义图标,因此它取代了 leading
属性,现在可以省略。 这个简单的 widget 替换是在不改变任何其他参数的情况下完成的,例如表示用户行为的图标,它们仍然可以按照之前的定义进行工作。
在 backdrop.dart
文件中,添加快捷方式,使用户可以从应用顶部栏的两个尾部图标返回到登录界面:为 IconButton
添加 semanticLabel
以用于新的目的。
// 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';
刷新,然后点击搜索或者其旁边的按钮来返回到登录界面。
在这四个 Codelab 的学习过程中,你已经了解到了如何使用 Material Components 来构建出能够表达品牌个性、风格独特且具有优雅的用户体验的应用。
下一步
本期 Codelab 是这个系列的最后一篇。你可以通过访问 Flutter Widgets 目录来探索更多 MDC-Flutter 的组件。
你可以将你的品牌图标替换为 AnimatedIcon 来达到一些特殊的目的。
如果你想学习到如何在 Flutter 应用中连接 Firebase 作为后端服务,可以查看 在 Flutter 里使用 Firebase 这个 Codelab。