- 原文地址:MDC-104 Flutter: Material Advanced Components (Flutter)
- 原文做者:codelabs.developers.google.com
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:DevMcryYu
- 校对者:iceytea
Material 组件(MDC)帮助开发者实现 Material Design。MDC 由谷歌团队的工程师和 UX 设计师创造,为 Android、iOS、Web 和 Flutter 提供不少美观实用的 UI 组件。html
在 MDC-103 教程中,自定义定制了 Material 组件(MDC)的颜色、高度、排版和形状来给你的应用设置样式。前端
Material Design 系统中的组件执行一些预约义的工做并具备必定特征,例如一个 button。然而一个 button 不只仅是用来给用户执行操做的,它能够用其形状、尺寸和颜色表达一种视觉体验,让用户知道它是可交互的,触摸或点击它时可能会有事情发生。android
Material Design 指南以设计师的角度来描述组件。它们描述了跨平台可用的基本功能以及构成每一个组件的基本元素。例如,一个背景包含一个背层内容、前层内容及其自己的内容、运动规则和显示选项。根据每一个应用的需求、用例和内容能够自定义每一个组件,包括传统的视图、控件以及你所处平台 SDK 的功能。ios
Material Design 指南命名了不少组件,但不是全部的组件均可以很好的被重用,所以没法在 MDC 中找到它们。你能够本身塑造这样的经历,实现使用传统代码自定义你的应用样式。git
本教程里,将把 Shrine 应用的 UI 修改为名为“背景”的两级展现。它包含一个菜单,列出了用于过滤在不对称网格中展现的产品的可选类别。在本教程中,你将使用以下 Flutter 组件:github
这是四篇教程中的最后一篇,它将指导你构建一个名为 Shrine 的应用。咱们建议你阅读每篇教程,跟随进度逐步完成此项目。后端
有关教程能够在这里找到:安全
要开始使用 Flutter 开发移动应用程序,你须要:bash
Flutter 的 IDE 工具适用于 Android Studio、IntelliJ IDEA Community(免费)和 IntelliJ IDEA Ultimate。架构
要在 iOS 上构建和运行 Flutter 应用程序,你须要知足如下要求:
要在 Android 上构建和运行 Flutter 应用程序,你须要知足如下要求:
重要提示:若是链接到计算机的 Android 手机上出现“容许 USB 调试”对话框,请启用始终容许今后计算机选项,而后单击肯定。
在继续本教程以前,请确保你的 SDK 处于正确的状态。若是以前安装过 Flutter SDK,则使用 flutter upgrade
来确保 SDK 处于最新版本。
flutter upgrade
复制代码
运行 flutter upgrade
将自动运行 flutter doctor
。若是这是首次安装 Flutter 且不需升级,那么请手动运行 flutter doctor
。查看显示的全部 ✓ 标记;这将会下载你须要的任何缺乏的 SDK 文件,并确保你的计算机配置无误以进行 Flutter 的开发。
flutter doctor
复制代码
若是你完成了 MDC-103,那么本教程所需的代码应该已经准备就绪。跳转到:添加背景菜单。
初始程序位于 material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series
目录下。
从 GitHub 克隆此项目,运行如下命令:
git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs
git checkout 104-starter\_and\_103-complete
复制代码
更多帮助:从 GitHub 克隆一个仓库
正确的分支
教程 MDC-101 到 MDC-104 在前一个基础上持续构建。MDC-103 的完整代码将是 MDC-104 的初始代码。代码被分红多个分支。要列出 GitHub 中的分支,使用以下命令:
git branch --list
想要查看完整代码,切换到
104-complete
分支。
创建你的项目
如下步骤默认你使用的是 Android Studio (IntelliJ)。
在终端中,导航到 material-components-flutter-codelabs
运行 flutter create mdc_100_series
打开 Android Studio。
若是你看到欢迎页面,单击打开已有的 Android Studio 项目。
material-components-flutter-codelabs/mdc_100_series
目录并单击打开,这将打开此项目。在构建项目一次以前,你能够忽略在分析中见到的任何错误。
../test/widget_test.dart
,删除它。提示:确保你已安装 Flutter 和 Dart 插件。
如下步骤默认你在 Android 模拟器或真实设备上进行测试。若是你安装了 Xcode,则也能够在 iOS 模拟器或设备上测试。
若是 Andorid 模拟器还没有运行,选择 Tools -> Android -> AVD Manager 来建立并运行一个模拟设备。若是 AVD 已存在,你能够直接在 IntelliJ 的设备选择器中启动模拟器,以下一步所示。
(对于 iOS 模拟器,若是它还没有运行,经过选择 Flutter Device Selection -> Open iOS Simulator 来在你的开发设备上启动它。)
若是你没法成功运行此应用程序,停下来解决你的开发环境问题。尝试导航到
material-components-flutter-codelabs
;若是你在终端中下载 .zip 文件,导航到material-components-flutter-codelabs-...
而后运行flutter create mdc_100_series
。
成功!上一篇教程中 Shrine 的登录页面应该在你的模拟器中运行了。你能够看到 Shrine 的 logo 和它下面的名称 "Shrine"。
若是应用没有更新,再次单击 “Play” 按钮,或者点击 “Play” 后的 “Stop”。
背景出如今全部其余内容和组件后面。它由两层组成:后层(显示操做和过滤器)和前层(用来显示内容)。你可使用背景来显示交互信息和操做,例如导航或内容过滤。
HomePage 的小部件将成为前层的内容。如今它有一个应用栏。咱们将应用栏移动到后层,这样 HomePage 将只包含 AsymmetricView。
在 home.dart
中,修改 build()
方法使其仅返回一个 AsymmetricView:
// TODO:返回一个 AsymmetricView(104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));
复制代码
建立名为 Backdrop 的小部件,使其包含 frontLayer
和 backLayer
。
backLayer
包含一个菜单,它容许你选择一个类别来过滤列表(currentCategory
)。因为咱们但愿菜单选择保持不变,所以咱们将 Backdrop 继承 StatefulWidget。
在 /lib
下添加名为 backdrop.dart
的文件:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'model/product.dart';
// TODO:添加速度常量(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:添加 _FrontLayer 类(104)
// TODO:添加 _BackdropTitle 类(104)
// TODO:添加 _BackdropState 类(104)
复制代码
导入 meta 包来添加 @required
标记。当构造函数中的属性没有默认值且不能为空的时候,用它来提醒你不能遗漏。注意,咱们在构造方法后再一次声明了传入的值的确不是 null
。
在 Backdrop 类定义下添加 _BackdropState
类:
// TODO:添加 _BackdropState 类(104)
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
// TODO:添加 AnimationController 部件(104)
// TODO:为 _buildStack 添加 BuildContext 和 BoxConstraints 参数(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,
titleSpacing: 0.0,
// TODO:用 IconButton 替换 leading 菜单图标(104)
// TODO:移除 leading 属性(104)
// TODO:使用 _BackdropTitle 参数建立标题(104)
leading: Icon(Icons.menu),
title: Text('SHRINE'),
actions: <Widget>[
// TODO:添加从尾部图标到登录页面的快捷方式(104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
// TODO:打开登陆(104)
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
// TODO:打开登陆(104)
},
),
],
);
return Scaffold(
appBar: appBar,
// TODO:返回一个 LayoutBuilder 部件(104)
body: _buildStack(),
);
}
}
复制代码
build()
方法像 HomePage 同样返回一个带有 app bar 的 Scaffold。可是 Scaffold 的主体是一个 Stack。Stack 的孩子能够重叠。每一个孩子的大小和位置都是相对于 Stack 的父级指定的。
如今在 ShrineApp 中添加一个 Backdrop 实例。
在 app.dart
中引入 backdrop.dart
及 model/product.dart
:
import 'backdrop.dart'; // 新增代码
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // 新增代码
import 'supplemental/cut_corners_border.dart';
复制代码
在 app.dart
中修改 ShrineApp 的 build()
方法。将 home:
改为以 HomePage 为 frontLayer
的 Backdrop。
// TODO:将 home: 改成使用 HomePage frontLayer 的 Backdrop(104)
home: Backdrop(
// TODO:使 currentCategory 持有 _currentCategory (104)
currentCategory: Category.all,
// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(),
// TODO:将 backLayer 的值改成 CategoryMenuPage(104)
backLayer: Container(color: kShrinePink100),
frontTitle: Text('SHRINE'),
backTitle: Text('MENU'),
),
复制代码
若是你点击运行按钮,你将会看到主页与应用栏已经出现了:
backLayer 在 frontLayer 的主页后面插入了一个新的粉色背景。
你可使用 Flutter Inspector 来验证在 Stack 里的主页后面确实有一个容器。就像这样:
如今你能够调整两个层的设计和内容。
在本小节,你将为 frontLayer 设置样式以在其左上角添加一个切片。
Material Design 将此类定制称为形状。Material 表面能够具备任意形状。形状为表面增长了重点和风格,可用于表达品牌特色。普通的矩形形状能够定制使其具备弯曲或成角度的角和边缘,以及任意数量的边。它们能够是对称的或不规则的。
斜角 Shrine logo 激发了 Shrine 应用的形状故事。形状故事是应用程序中应用的形状的常见用法。例如,徽标形状在应用了形状的登陆页面元素中回显。在本小节,您将在左上角使用倾斜切片作为前层设置样式。
在 backdrop.dart
中,添加新的 _FrontLayer
类:
// TODO:添加 _FrontLayer 类(104)
class _FrontLayer extends StatelessWidget {
// TODO:添加 on-tap 回调(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:添加 GestureDetector(104)
Expanded(
child: child,
),
],
),
);
}
}
复制代码
而后在 BackdropState 的 _buildStack()
方法里将 front layer 包裹在 _FrontLayer
内:
Widget _buildStack() {
// TODO:建立一个 RelativeRectTween 动画(104)
return Stack(
key: _backdropKey,
children: <Widget>[
widget.backLayer,
// TODO:添加 PositionedTransition(104)
// TODO:在 _FrontLayer 中包裹 front layer(104)
_FrontLayer(child: widget.frontLayer),
],
);
}
复制代码
重载。
咱们给 Shrine 的主表面定制了一个形状。因为表面具备高度,用户能够看到白色前层后面有东西。让咱们添加一个动做,以便用户能够看到背景的背景层。
动做是一种可让你的应用变得更真实的方式。它能够是大且夸张的、小且微妙的,亦或是介于二者之间的。但须要注意的是动做的形式必定要适合使用场景。屡次重复的有规律的动做要精细小巧,才不会分散用户的注意力或占用太多时间。适当的状况,如用户第一次打开应用时,长时的动做可能会更引人注目,一些动画也能够帮助用户了解如何使用您的应用程序。
在 backdrop.dart
的顶部,其余类函数外,添加一个常量来表示咱们须要的动画执行的速度:
// TODO:添加速度常数(104)
const double _kFlingVelocity = 2.0;
复制代码
在 _BackdropState
中添加 AnimationController
部件,在 initState()
函数中实例化它,并将其部署在 state 的 dispose()
函数中:
// TODO:添加 AnimationController 部件(104)
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
// TODO:重写 didUpdateWidget(104)
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// TODO:添加函数以肯定并改变 front layer 可见性(104)
复制代码
部件生命周期
仅在部件成为其渲染树的一部分以前会调用一次
initState()
方法。只有在部件从树中移除时才会调用一次dispose()
方法。
AnimationController 用来配合 Animation,并提供播放、反向和中止动画的 API。如今咱们须要使用某个方法来移动它。
添加函数以肯定并改变 front layer 的可见性:
// TODO:添加函数以肯定并改变 front layer 的可见性(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 部件中。当 back layer 不可见时,此部件将从语义树中剔除 backLayer 的菜单项。
return Stack(
key: _backdropKey,
children: <Widget>[
// TODO:将 backLayer 包裹在 ExcludeSemantics 部件中(104)
ExcludeSemantics(
child: widget.backLayer,
excluding: _frontLayerVisible,
),
...
复制代码
修改 _buildStack()
方法使其持有一个 BuildContext 和 BoxConstraints。同时包含一个使用 RelativeRectTween 动画的 PositionedTransition:
// TODO:为 _buildStack 添加 BuildContext 和 BoxConstraints 参数(104)
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
// TODO:建立一个 RelativeRectTween 动画(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:添加一个 PositionedTransition(104)
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
// TODO:在 _BackdropState 上实现 onTap 属性(104)
child: widget.frontLayer,
),
),
],
);
}
复制代码
最后,返回一个使用 _buildStack
做为其 builder 的 LayoutBuilder 部件,而不是为 Scaffold 的主体调用 _buildStack
函数:
return Scaffold(
appBar: appBar,
// TODO:返回一个 LayoutBuilder 部件(104)
body: LayoutBuilder(builder: _buildStack),
);
复制代码
咱们使用 LayoutBuilder 将 front/back 堆栈的构建延迟到布局阶段,以便咱们能够合并背景的实际总体高度。LayoutBuilder 是一个特殊的部件,其构建器回调提供了大小约束。
LayoutBuilder
部件树经过遍历叶结点来组织布局。约束在树下传递,可是在叶结点根据约束返回其大小以前一般不会计算大小。叶子点没法知道它的父母的大小,由于它还没有计算。
当部件必须知道其父部件的大小以便自行布局(且父部件大小不依赖于子部件)时,LayoutBuilder 就派上用场了。它使用一个方法来返回部件。
了解有关更多信息,请查看 LayoutBuilder 类文档。
在 build()
方法中,将应用栏中的前导菜单图标转换为 IconButton,并在点击按钮时使用它来切换 front layer 的可见性。
// TODO:用 IconButton 替换 leading 菜单图标(104)
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: _toggleBackdropLayerVisibility,
),
复制代码
在模拟器中重载并点击菜单按钮。
front layer 在向下移动(滑动)。但若是向下看,则会出现红色错误和溢出错误。这是由于 AsymmetricView 被这个动画挤压并变小,反过来使得 Column 的空间更小。最终,Column 不能用给定的空间自行排列并致使错误。若是咱们用 ListView 替换 Column,则移动时列的尺寸仍然保持不变。
在 supplemental/product_columns.dart
中,将 OneProductCardColumn
的 Column 替换成 ListView:
class OneProductCardColumn extends StatelessWidget {
OneProductCardColumn({this.product});
final Product product;
@override
Widget build(BuildContext context) {
// TODO:用 ListView 替换 Column(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
的计算方式,并将 TwoProductCardColumn
中的 Column 替换成 ListView:
// TODO:修改 imageAspectRatio 的计算方式(104)
double imageAspectRatio =
(heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages)
? constraints.biggest.width / heightOfImages
: 33 / 49;
// TODO:用 ListView 替换 Column(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 部件从 stateless 转换成 stateful。
ShrineApp.
_ShrineAppState
)。要从 IDE 主菜单执行此操做,请选择 Refactor > Rename。或者在代码中,您能够高亮显示类名 ShrineAppState,而后右键单击并选择 Refactor > Rename。输入 _ShrineAppState
以使该类成为私有。在 app.dart
中,为选择的类别添加一个变量 _ShrineAppState
,并在点击时添加一个回调:
// TODO:将 ShrineApp 转换成 stateful 部件(104)
class _ShrineAppState extends State<ShrineApp> {
Category _currentCategory = Category.all;
void _onCategoryTap(Category category) {
setState(() {
_currentCategory = category;
});
}
复制代码
而后将 back layer 修改成 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:让 currentCategory 字段持有 _currentCategory(104)
currentCategory: _currentCategory,
// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(),
// TODO:将 backLayer 修改为 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:为 Category 添加一个变量(104)
final Category category;
const HomePage({this.category: Category.all});
@override
Widget build(BuildContext context) {
// TODO:为 Category 添加一个变量并将其传递给 AsymmetricView(104)
return AsymmetricView(products: ProductsRepository.loadProducts(category));
}
}
复制代码
在 app.dart
中为 frontLayer
传递 _currentCategory
:
// TODO:为 frontLayer 传递 _currentCategory(104)
frontLayer: HomePage(category: _currentCategory),
复制代码
重载。点击模拟器中的菜单按钮并选择一个类别。
点击菜单图标以查看产品。他们被过滤了!
在 backdrop.dart
中,为 BackdropState
重写 didUpdateWidget()
方法:
// TODO:为 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 添加一个 on-tap 回调:
class _FrontLayer extends StatelessWidget {
// TODO:添加 on-tap 回调(104)
const _FrontLayer({
Key key,
this.onTap, // 新增代码
this.child,
}) : super(key: key);
final VoidCallback onTap; // 新增代码
final Widget child;
复制代码
而后将一个 GestureDetector 添加到 _FrontLayer
的孩子 Column 的子节点中:
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// TODO:添加一个 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:在 _BackdropState 中实现 onTap 属性(104)
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
复制代码
重载并点击 front layer 的顶部。每次你点击 front layer 顶部时都它应该打开或者关闭。
品牌肖像也应该延伸到熟悉的图标。让咱们自定义显示图标并将其与咱们的标题合并,以得到独特的品牌外观。
在 backdrop.dart
中,新建 _BackdropTitle
类。
// TODO:添加 _BackdropTitle 类(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>[
// 品牌图标
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')),
)]),
),
),
// 在这里,咱们在 backTitle 和 frontTitle 之间是实现自定义的交叉淡入淡出效果
// 这使得两个文本之间可以平滑过渡。
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
- assets/slanted_menu.png
- packages/shrine_images/0-0.jpg
复制代码
移除 AppBar
builder 中的 leading
属性。这样才能在原始 leading
部件的位置显示自定义品牌图标。listenable
动画和品牌图标的 onPress
处理将传递给 _BackdropTitle
。frontTitle
和 backTitle
也会被传递,以便将它们显示在背景标题中。AppBar
的 title
参数以下所示:
// TODO:使用 _BackdropTitle 参数建立标题(104)
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
复制代码
品牌图标在 _BackdropTitle
中建立。它包含一组动画图标:倾斜的菜单和钻石,它包裹在 IconButton
中,以即可以按下它。而后将 IconButton
包装在 SizedBox
中,以便为图标水平运动腾出空间。
Flutter 的 "everything is a widget" 架构容许更改默认 AppBar
的布局,而无需建立全新的自定义 AppBar
小部件。title
参数最初是一个 Text
部件,能够用更复杂的 _BackdropTitle
替换。因为 _BackdropTitle
还包含自定义图标,所以它取代了 leading
属性,如今能够省略。这个简单的部件替换是在不改变任何其余参数的状况下完成的,例如动做图标,它们能够继续运行。
在 backdrop.dart
中,从应用栏中的两个尾部图标向登陆屏幕添加一个快捷方式:更改图标的 semanticLabel
以反映其新用途。
// TODO:添加从尾部图标到登录页面的快捷方式(104)
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'login', // 新增代码
),
onPressed: () {
// TODO:打开登录(104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'login', // 新增代码
),
onPressed: () {
// TODO:打开登陆(104)
Navigator.push(
context,
MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
);
},
),
复制代码
若是你尝试重载将收到错误消息。导入 login.dart
以修复错误:
import 'login.dart';
复制代码
重载应用并点击搜索或调整按钮以返回登陆屏幕。
经过四篇教程,你已经了解了如何使用 Material 组件来构建表达品牌个性和风格的独特,优雅的用户体验。
完整的 MDC-104 应用可在
104-complete
分支中找到。您可使用该分支中的版本测试你的应用。
MDC-104 到此已经完成。你能够访问 Flutter Widget 目录以在 MDC-Flutter 中探索更多组件。
对于进阶的目标,尝试使用 AnimatedIcon 替换品牌图标。
要了解如何将应用链接到 Firebase 以得到后端支持,请参阅 Flutter 中的 Firebase。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。