設(shè)計(jì)給的效果如下:

拿到設(shè)計(jì)后,先把整體拆分成幾個(gè)部分:
- “提醒頁(yè)面”,顯示在屏幕上方的文字提醒頁(yè)面,不會(huì)覆蓋原路由頁(yè)面。
- “路由導(dǎo)航”,使用Flutter的路由與導(dǎo)航組件來(lái)推(
push)提醒頁(yè)面。 - “倒計(jì)時(shí)拋”,使用Flutter的倒計(jì)時(shí)組件自動(dòng)拋(
pop)提醒頁(yè)面。 - “過(guò)渡動(dòng)畫(huà)”,為推(
push)和拋(pop)提醒頁(yè)面的過(guò)程添加動(dòng)畫(huà)效果。
然后就可以開(kāi)始進(jìn)行編碼了。
第1步:繪制組件樹(shù)

第2步:實(shí)現(xiàn)“提醒頁(yè)面”
屏幕頂部提醒頁(yè)面應(yīng)該只占屏幕一小部分,而且不能遮蓋住原本的路由頁(yè)面,所以你不能使用腳手架(Scaffold)組件,因?yàn)樗鼤?huì)占據(jù)整個(gè)屏幕。直接使用手勢(shì)探測(cè)器(GestureDetector)組件作為頂部提醒組件的根組件,為每一個(gè)子組件都設(shè)置固定的高度,使剩下的屏幕高度都是空白的。
import 'dart:async';
import 'package:flutter/material.dart';
// TODO: 第3步:實(shí)現(xiàn)“路由導(dǎo)航”。
/// 自定義的頂部提醒組件。
class TopReminder extends StatefulWidget {
/// 提醒文本。
final String reminderText;
TopReminder({
@required
this.reminderText,
});
@override
_TopReminderState createState() => _TopReminderState();
}
/// 與自定義的頂部提醒組件關(guān)聯(lián)的狀態(tài)子類(lèi)。
class _TopReminderState extends State<TopReminder> {
// TODO: 第4步:實(shí)現(xiàn)“倒計(jì)時(shí)拋”。
@override
Widget build(BuildContext context) {
return GestureDetector(
child: Column(
children: <Widget>[
Container(
// 雙精度(`double`)類(lèi)的無(wú)窮(`infinity`)常量,最大寬度。
width: double.infinity,
height: 85.0,
color: const Color(0xFFFF6F6F),
child: Align(
alignment: Alignment.bottomCenter,
// 使用材料(`Material`)組件來(lái)避免文本下方的黃色線(xiàn)條。
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`)屬性,將子組件放在這個(gè)填充內(nèi)。
padding: EdgeInsets.only(bottom: 7.0),
),
Container(
height: 3.0,
color: const Color(0xFF4A4A4A),
),
// 不透明度(`Opacity`)組件,使子組件部分透明。
Opacity(
// 不透明度(`opacity`)屬性,縮放子組件的阿爾法通道(`alpha`)值的分?jǐn)?shù)。
// 不透明度為1.0是完全不透明的,不透明度為0.0是完全透明的(即不可見(jiàn))。
opacity: 0.5,
child: Container(
height: 4.0,
color: const Color(0xFFCCCCCC),
),
),
],
),
// TODO: 第3步:實(shí)現(xiàn)“路由導(dǎo)航”,點(diǎn)擊提醒頁(yè)面時(shí)返回。
);
}
}
第3步:實(shí)現(xiàn)“路由導(dǎo)航”
通過(guò)導(dǎo)航器(Navigator)組件和頁(yè)面路由生成器(PageRouteBuilder)組件,可以實(shí)現(xiàn)打開(kāi)頂部提醒頁(yè)面的方法,通過(guò)這個(gè)openTopReminder方法,你可以在任意路由頁(yè)面調(diào)用頂部提醒頁(yè)面。
// TODO: 第3步:實(shí)現(xiàn)“路由導(dǎo)航”。
/// 打開(kāi)頂部提醒頁(yè)面。
void openTopReminder(context, String reminderText) {
// 導(dǎo)航器(`Navigator`)組件,用于管理具有堆棧規(guī)則的一組子組件。
// 許多應(yīng)用程序在其窗口組件層次結(jié)構(gòu)的頂部附近有一個(gè)導(dǎo)航器,以便使用疊加顯示其邏輯歷史記錄,
// 最近訪(fǎng)問(wèn)過(guò)的頁(yè)面可視化地顯示在舊頁(yè)面之上。使用此模式,
// 導(dǎo)航器可以通過(guò)在疊加層中移動(dòng)組件來(lái)直觀地從一個(gè)頁(yè)面轉(zhuǎn)換到另一個(gè)頁(yè)面。
// 類(lèi)似地,導(dǎo)航器可用于通過(guò)將對(duì)話(huà)框窗口組件放置在當(dāng)前頁(yè)面上方來(lái)顯示對(duì)話(huà)框。
// 導(dǎo)航器(`Navigator`)組件的關(guān)于(`of`)方法,來(lái)自此類(lèi)的最近實(shí)例的狀態(tài),它包含給定的上下文。
// 導(dǎo)航器(`Navigator`)組件的推(`push`)方法,將給定路徑推送到最緊密包圍給定上下文的導(dǎo)航器。
Navigator.of(context).push(
// 頁(yè)面路由生成器(`PageRouteBuilder`)組件,用于根據(jù)回調(diào)定義一次性頁(yè)面路由的實(shí)用程序類(lèi)。
PageRouteBuilder(
// 轉(zhuǎn)換完成后路由是否會(huì)遮蓋以前的路由。
opaque: false,
// 頁(yè)面構(gòu)建器(`pageBuilder`)屬性,用于構(gòu)建路徑的主要內(nèi)容。
pageBuilder: (BuildContext context, _, __) {
return TopReminder(reminderText: reminderText);
},
// TODO: 第5步:實(shí)現(xiàn)“過(guò)渡動(dòng)畫(huà)”。
),
);
}
給手勢(shì)探測(cè)器(GestureDetector)組件添加一個(gè)點(diǎn)擊事件,并在回調(diào)函數(shù)中拋棄頂部提醒頁(yè)面,以返回原來(lái)的路由頁(yè)面。
// TODO: 第3步:實(shí)現(xiàn)“路由導(dǎo)航”,點(diǎn)擊提醒頁(yè)面時(shí)返回。
onTap: () {
// TODO: 第4步:實(shí)現(xiàn)“倒計(jì)時(shí)拋”,關(guān)閉倒計(jì)時(shí)。
Navigator.of(context).pop(true);
},
第4步:實(shí)現(xiàn)“倒計(jì)時(shí)拋”
除了用戶(hù)手動(dòng)點(diǎn)擊頂部提醒頁(yè)面外,還需要一個(gè)計(jì)時(shí)器來(lái)定時(shí)拋棄頂部提醒頁(yè)面,實(shí)現(xiàn)自動(dòng)返回原來(lái)的路由頁(yè)面。
// TODO: 第4步:實(shí)現(xiàn)“倒計(jì)時(shí)拋”。
/// 倒計(jì)時(shí)的計(jì)時(shí)器。
Timer _timer;
@override
void initState() {
super.initState();
_startTimer();
}
/// 啟動(dòng)倒計(jì)時(shí)的計(jì)時(shí)器。
_startTimer() {
_timer = Timer(
// 持續(xù)時(shí)間參數(shù)。
Duration(seconds: 2),
// 回調(diào)函數(shù)參數(shù)。
() {
Navigator.of(context).pop(true);
},
);
}
/// 取消倒計(jì)時(shí)的計(jì)時(shí)器。
void _cancelTimer() {
// 計(jì)時(shí)器(`Timer`)組件的取消(`cancel`)方法,取消計(jì)時(shí)器。
_timer?.cancel();
}
在手勢(shì)探測(cè)器(GestureDetector)組件的點(diǎn)擊事件中調(diào)用關(guān)閉倒計(jì)時(shí)的_cancelTimer方法,避免用戶(hù)手動(dòng)返回原來(lái)的路由頁(yè)面以后,倒計(jì)時(shí)任務(wù)仍在運(yùn)行,導(dǎo)致應(yīng)用程序拋出異常信息。
// TODO: 第4步:實(shí)現(xiàn)“倒計(jì)時(shí)拋”,關(guān)閉倒計(jì)時(shí)。
// 點(diǎn)擊提醒頁(yè)面時(shí)關(guān)閉倒計(jì)時(shí)并返回。
_cancelTimer();
第5步:實(shí)現(xiàn)“過(guò)渡動(dòng)畫(huà)”
通過(guò)淡出過(guò)渡(FadeTransition)組件使原來(lái)的路由頁(yè)面自然淡出,再通過(guò)滑動(dòng)過(guò)渡(SlideTransition)組件使新的路由頁(yè)面從屏幕頂部上方開(kāi)始,向屏幕下方平滑移動(dòng)。返回原來(lái)的路由頁(yè)面時(shí),也是同樣的效果,不同的是反方向播放動(dòng)畫(huà)。
// TODO: 第5步:實(shí)現(xiàn)“過(guò)渡動(dòng)畫(huà)”。
// 轉(zhuǎn)換生成器(`transitionsBuilder`)屬性,用于構(gòu)建路徑的轉(zhuǎn)換。
transitionsBuilder: (_, Animation<double> animation, __, Widget child) {
// 淡出過(guò)渡(`FadeTransition`)組件,動(dòng)畫(huà)組件的不透明度。
// https://docs.flutter.io/flutter/widgets/FadeTransition-class.html
return FadeTransition(
// 不透明度(`opacity`)屬性,控制子組件不透明度的動(dòng)畫(huà)。
opacity: animation,
// 滑動(dòng)過(guò)渡(`SlideTransition`)組件,動(dòng)畫(huà)組件相對(duì)于其正常位置的位置。
// https://docs.flutter.io/flutter/widgets/SlideTransition-class.html
child: SlideTransition(
// 位置(`position`)屬性,控制子組件位置的動(dòng)畫(huà)。
// 兩者之間(`Tween`)類(lèi),開(kāi)始值和結(jié)束值之間的線(xiàn)性插值。
// 偏移(`Offset`)類(lèi),不可變的2D浮點(diǎn)偏移量。
position: Tween<Offset>(
// 兩者之間(`Tween`)類(lèi)的開(kāi)始(`begin`)屬性,此變量在動(dòng)畫(huà)開(kāi)頭的值。
begin: Offset(0.0, -0.3),
// 兩者之間(`Tween`)類(lèi)的結(jié)束(`end`)屬性,此變量在動(dòng)畫(huà)結(jié)束時(shí)的值。
end: Offset.zero,
// 兩者之間(`Tween`)類(lèi)的活躍(`animate`)方法,返回由給定動(dòng)畫(huà)驅(qū)動(dòng)但接受由此對(duì)象確定的值的新動(dòng)畫(huà)。
).animate(animation),
child: child,
),
);
}
第6步:還原效果
