设计给的效果如下:
拿到设计后,先把整体拆分成几个部分:
- “提醒页面”,显示在屏幕上方的文字提醒页面,不会覆盖原路由页面。
- “路由导航”,使用Flutter的路由与导航组件来推(
push
)提醒页面。 - “倒计时抛”,使用Flutter的倒计时组件自动抛(
pop
)提醒页面。 - “过渡动画”,为推(
push
)和抛(pop
)提醒页面的过程添加动画效果。
然后就可以开始进行编码了。
第1步:绘制组件树
第2步:实现“提醒页面”
屏幕顶部提醒页面应该只占屏幕一小部分,而且不能遮盖住原本的路由页面,所以你不能使用脚手架(Scaffold
)组件,因为它会占据整个屏幕。直接使用手势探测器(GestureDetector
)组件作为顶部提醒组件的根组件,为每一个子组件都设置固定的高度,使剩下的屏幕高度都是空白的。
import 'dart:async';
import 'package:flutter/material.dart';
// TODO: 第3步:实现“路由导航”。
/// 自定义的顶部提醒组件。
class TopReminder extends StatefulWidget {
/// 提醒文本。
final String reminderText;
TopReminder({
@required
this.reminderText,
});
@override
_TopReminderState createState() => _TopReminderState();
}
/// 与自定义的顶部提醒组件关联的状态子类。
class _TopReminderState extends State {
// TODO: 第4步:实现“倒计时抛”。
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Column(
children: [
Container(
// 双精度(`double`)类的无穷(`infinity`)常量,最大宽度。
width: double.infinity,
height: 85.0,
color: const Color(0xFFFF6F6F),
child: Align(
alignment: Alignment.bottomCenter,
// 使用材料(`Material`)组件来避免文本下方的黄色线条。
child: Material(
color: const Color(0xFFFF6F6F),
child: Text(
widget.reminderText,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18.0,
color: const Color(0xFF282828),
),
),
),
),
// 容器(`Container`)组件的填充(`padding`)属性,将子组件放在这个填充内。
padding: EdgeInsets.only(bottom: 7.0),
),
Container(
height: 3.0,
color: const Color(0xFF4A4A4A),
),
// 不透明度(`Opacity`)组件,使子组件部分透明。
Opacity(
// 不透明度(`opacity`)属性,缩放子组件的阿尔法通道(`alpha`)值的分数。
// 不透明度为1.0是完全不透明的,不透明度为0.0是完全透明的(即不可见)。
opacity: 0.5,
child: Container(
height: 4.0,
color: const Color(0xFFCCCCCC),
),
),
],
),
// TODO: 第3步:实现“路由导航”,点击提醒页面时返回。
);
}
}
第3步:实现“路由导航”
通过导航器(Navigator
)组件和页面路由生成器(PageRouteBuilder
)组件,可以实现打开顶部提醒页面的方法,通过这个openTopReminder
方法,你可以在任意路由页面调用顶部提醒页面。
// TODO: 第3步:实现“路由导航”。
/// 打开顶部提醒页面。
void openTopReminder(context, String reminderText) {
// 导航器(`Navigator`)组件,用于管理具有堆栈规则的一组子组件。
// 许多应用程序在其窗口组件层次结构的顶部附近有一个导航器,以便使用叠加显示其逻辑历史记录,
// 最近访问过的页面可视化地显示在旧页面之上。使用此模式,
// 导航器可以通过在叠加层中移动组件来直观地从一个页面转换到另一个页面。
// 类似地,导航器可用于通过将对话框窗口组件放置在当前页面上方来显示对话框。
// 导航器(`Navigator`)组件的关于(`of`)方法,来自此类的最近实例的状态,它包含给定的上下文。
// 导航器(`Navigator`)组件的推(`push`)方法,将给定路径推送到最紧密包围给定上下文的导航器。
Navigator.of(context).push(
// 页面路由生成器(`PageRouteBuilder`)组件,用于根据回调定义一次性页面路由的实用程序类。
PageRouteBuilder(
// 转换完成后路由是否会遮盖以前的路由。
opaque: false,
// 页面构建器(`pageBuilder`)属性,用于构建路径的主要内容。
pageBuilder: (BuildContext context, _, __) {
return TopReminder(reminderText: reminderText);
},
// TODO: 第5步:实现“过渡动画”。
),
);
}
给手势探测器(GestureDetector)组件添加一个点击事件,并在回调函数中抛弃顶部提醒页面,以返回原来的路由页面。
// TODO: 第3步:实现“路由导航”,点击提醒页面时返回。
onTap: () {
// TODO: 第4步:实现“倒计时抛”,关闭倒计时。
Navigator.of(context).pop(true);
},
第4步:实现“倒计时抛”
除了用户手动点击顶部提醒页面外,还需要一个计时器来定时抛弃顶部提醒页面,实现自动返回原来的路由页面。
// TODO: 第4步:实现“倒计时抛”。
/// 倒计时的计时器。
Timer _timer;
@override
void initState() {
super.initState();
_startTimer();
}
/// 启动倒计时的计时器。
_startTimer() {
_timer = Timer(
// 持续时间参数。
Duration(seconds: 2),
// 回调函数参数。
() {
Navigator.of(context).pop(true);
},
);
}
/// 取消倒计时的计时器。
void _cancelTimer() {
// 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。
_timer?.cancel();
}
在手势探测器(GestureDetector)组件的点击事件中调用关闭倒计时的_cancelTimer
方法,避免用户手动返回原来的路由页面以后,倒计时任务仍在运行,导致应用程序抛出异常信息。
// TODO: 第4步:实现“倒计时抛”,关闭倒计时。
// 点击提醒页面时关闭倒计时并返回。
_cancelTimer();
第5步:实现“过渡动画”
通过淡出过渡(FadeTransition
)组件使原来的路由页面自然淡出,再通过滑动过渡(SlideTransition
)组件使新的路由页面从屏幕顶部上方开始,向屏幕下方平滑移动。返回原来的路由页面时,也是同样的效果,不同的是反方向播放动画。
// TODO: 第5步:实现“过渡动画”。
// 转换生成器(`transitionsBuilder`)属性,用于构建路径的转换。
transitionsBuilder: (_, Animation animation, __, Widget child) {
// 淡出过渡(`FadeTransition`)组件,动画组件的不透明度。
// https://docs.flutter.io/flutter/widgets/FadeTransition-class.html
return FadeTransition(
// 不透明度(`opacity`)属性,控制子组件不透明度的动画。
opacity: animation,
// 滑动过渡(`SlideTransition`)组件,动画组件相对于其正常位置的位置。
// https://docs.flutter.io/flutter/widgets/SlideTransition-class.html
child: SlideTransition(
// 位置(`position`)属性,控制子组件位置的动画。
// 两者之间(`Tween`)类,开始值和结束值之间的线性插值。
// 偏移(`Offset`)类,不可变的2D浮点偏移量。
position: Tween(
// 两者之间(`Tween`)类的开始(`begin`)属性,此变量在动画开头的值。
begin: Offset(0.0, -0.3),
// 两者之间(`Tween`)类的结束(`end`)属性,此变量在动画结束时的值。
end: Offset.zero,
// 两者之间(`Tween`)类的活跃(`animate`)方法,返回由给定动画驱动但接受由此对象确定的值的新动画。
).animate(animation),
child: child,
),
);
}