在上一期,咱們已經完成了點開動畫的編寫和執行,若是有仔細看完的小夥伴會發現,其中的動畫效果不止擴散這麼簡單,本篇就來繼續研究其他的動畫交互。git
做爲一個炫(pin)酷(ru)的頁面,頁面中的交互也很是的重要。在本篇,我將進一步說明頁面內各個位置的交互細節,從而帶着各位作一個不將就的強迫症~github
效果圖: 數組
完整demo及組件已上傳至項目,走過路過留個star~bash
頁面中的交互主要包含三個觸發位置:markdown
接下來將逐點說明如何實現。app
咱們知道在Flutter中,頁面要返回時,會執行Navigator.maybePop
的方法,使頁面返回。爲了攔截路由pop
,Flutter提供了WillPopScope
來攔截返回行爲,咱們只須要註冊onWillPop
方法,就能夠在pop
前執行代碼。async
bool _popping = false;
Future<bool> willPop() async {
/// 等待返回動畫的執行
await backDropFilterAnimate(context, false);
/// 判斷_popping從而避免重複觸發pop
if (!_popping) {
_popping = true;
await Future.delayed(Duration(milliseconds: _animateDuration), () {
Navigator.of(context).pop();
});
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: WillPopScope(
/// 綁定willPop方法
onWIllPop: willPop,
child: wrapper(
context,
child: widget.child,
),
),
);
}
複製代碼
如此咱們就輕鬆愉快地攔截了路由~ide
思考退出動畫和跳轉動畫的關係,咱們立馬就能夠想到,跳轉和退出的動畫是相反的,也就是說,逆向執行跳轉的動畫,就能獲得一個退出動畫。佈局
這時咱們來回顧一下上一期的跳轉動畫:post
void backDropFilterAnimate(BuildContext context) async {
final Size s = MediaQuery.of(context).size;
_backDropFilterController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _backDropFilterCurve = CurvedAnimation(
parent: _backDropFilterController,
curve: Curves.easeInOut,
);
_backDropFilterAnimation = Tween(
begin: 0.0,
end: pythagoreanTheorem(s.width, s.height) * 2,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
_backDropFilterController.forward();
}
複製代碼
要想以相反的方向執行動畫,咱們加入一個參數bool forward
:
void backDropFilterAnimate(BuildContext context, bool forward)
使用forward
來控制begin
和end
,達到執行的效果。同時對forward
進行判斷,若是爲false
則嘗試暫停動畫:
void backDropFilterAnimate(BuildContext context, bool forward) {
/.../
if (!forward) _backDropFilterController?.stop();
_backDropFilterAnimation = Tween(
/// 三元運算賦值
begin: forward ? 0.0 : _backdropFilterSize,
end: forward ? pythagoreanTheorem(s.width, s.height) * 2 : 0.0,
).animate(_backDropFilterCurve)
..addListener(() {
setState(() {
_backdropFilterSize = _backDropFilterAnimation.value;
});
});
/.../
}
複製代碼
看到這裏可能會有小夥伴問了,AnimateController
明明提供了reverse
方法用於反向,爲何還要使用一個bool
來控制動畫執行方向呢?
緣由在於當使用reverse
時,控制器會將begin
和end
對調來執行動畫,但當咱們執行退出動畫時,圓形不必定已經徹底覆蓋,因此經過使用forward
來判斷方向,能夠使未徹底覆蓋的動畫從中止處反向執行,不會形成閃爍的狀況。
至此,跳轉和退出動畫已經完美完成。
根據效果圖,在頁面的底部,會提供一個帶有旋轉動畫返回按鈕,點擊能夠返回。
因爲個人頁面時點擊加號觸發的,因此這裏我引入了bottomHeight
,用來肯定加號的位置。從效果圖能夠看到個人底部導航欄,它的高度咱們假設是60.0
,那按鈕的位置如何定義呢?
final double bottomHeight = 60.0;
/.../
Widget popButton() {
return SizedBox(
/// 此處假設爲60.0
width: widget.bottomHeight,
height: widget.bottomHeight,
child: Center(
/// 套手勢監聽,並設定監聽行爲
child: GestureDetector(
behavior: HitTestBehavior.opaque,
child: Icon(
Icons.add,
color: Colors.grey
),
onTap: willPop,
),
),
);
}
複製代碼
將它放入佈局中:
Stack(
/.../
children: <Widget>[
Positioned(
/// 將按鈕控件固定在視圖底部中央
left: 0.0,
right: 0.0,
bottom: 0.0,
child: popButton(),
),
],
)
複製代碼
按鈕定位完成,這時咱們開始設計動畫。按鈕一共須要兩組動畫,一組是旋轉,一組是淡入淡出。
/// 初始化按鈕旋轉的角度
final double bottomButtonRotateDegree = 45.0;
/// 旋轉動畫相關
Animation<double> _popButtonAnimation;
AnimationController _popButtonController;
/// 淡入淡出相關
Animation<double> _popButtonOpacityAnimation;
AnimationController _popButtonOpacityController;
void popButtonAnimate(context, bool forward) {
/// 與背景相同,判斷正反執行
if (!forward) {
_popButtonController?.stop();
_popButtonOpacityController?.stop();
}
/// 轉換按鈕實際旋轉角度
final double rotateDegree =
widget.bottomButtonRotateDegree * (math.pi / 180);
///
_popButtonOpacityController = _popButtonController = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
Animation _popButtonCurve = CurvedAnimation(
parent: _popButtonController,
curve: Curves.easeInOut,
);
_popButtonAnimation = Tween(
begin: forward ? 0.0 : _popButtonRotateAngle,
end: forward ? rotateDegree : 0.0,
).animate(_popButtonCurve)
..addListener(() {
setState(() {
_popButtonRotateAngle = _popButtonAnimation.value;
});
});
/// 設定透明度最小值爲0.01,防止背景顯示錯誤
_popButtonOpacityAnimation = Tween(
begin: forward ? 0.01 : _popButtonOpacity,
end: forward ? 1.0 : 0.01,
).animate(_popButtonCurve)
..addListener(() {
setState(() {
_popButtonOpacity = _popButtonOpacityAnimation.value;
});
});
_popButtonController.forward();
_popButtonOpacityController.forward();
}
複製代碼
按鈕動畫構建完成,咱們將它放到背景動畫中一塊兒執行:
Future backDropFilterAnimate(BuildContext context, bool forward) async {
/.../
/// 使用相同的forward控制方向
popButtonAnimate(context, forward);
/.../
}
複製代碼
至此,按鈕的動畫會跟着背景一塊兒聯動了,十分完美~
但,彆着急結束,咱們還有內容的動畫定製沒有完成,若是不須要如效果圖通常的元素動畫,能夠出門右轉~
從效果圖咱們能夠看到,兩個操做項是依次淡入出現,而且帶有必定的垂直位移。這時問題出現了:個人操做項數量不肯定,難道每個操做項我都要專門寫一個動畫嗎?
答案是:對了一半。爲何這麼說?咱們確實須要寫操做項的動畫,但咱們不須要重複地去寫每個操做項,只須要經過封裝操做項的內容,將動畫全部相關內容也組成數個List
,問題就簡單了不少。
以效果圖爲例,我有兩個操做項,先進行聲明。
List<String> itemTitles = ["動態", "掃一掃"];
List<String> itemIcons = ["subscriptedAccount", "scan"];
List<Color> itemColors = [Colors.orange, Colors.teal];
List<Function> itemOnTap = [...];
複製代碼
將操做項全部的信息存儲在四個數組中。接下來咱們建立兩組動畫共8個數組的相關變量。
/// 操做項垂直偏移量
List<double> _itemOffset;
/// 操做項偏移動畫
List<Animation<double>> _itemAnimations;
/// 操做項偏移動畫曲線
List<CurvedAnimation> _itemCurveAnimations;
/// 操做項偏移動畫控制器
List<AnimationController> _itemAnimateControllers;
/// 操做項透明度
List<double> _itemOpacity;
/// 操做項透明度動畫
List<Animation<double>> _itemOpacityAnimations;
/// 操做項透明度動畫曲線
List<CurvedAnimation> _itemOpacityCurveAnimations;
/// 操做項透明度動畫控制器
List<AnimationController> _itemOpacityAnimateControllers;
複製代碼
那麼,該怎麼初始化動畫呢?
void initItemsAnimation() {
/// 根據操做項內容,初始化動畫相關變量
_itemOffset = <double>[for (int i=0; i<itemTitles.length; i++) 0.0];
_itemAnimations = List<Animation<double>>(itemTitles.length);
_itemCurveAnimations = List<CurvedAnimation>(itemTitles.length);
_itemAnimateControllers = List<AnimationController>(itemTitles.length);
_itemOpacity = <double>[for (int i=0; i<itemTitles.length; i++) 0.01];
_itemOpacityAnimations = List<Animation<double>>(itemTitles.length);
_itemOpacityCurveAnimations = List<CurvedAnimation>(itemTitles.length); _itemOpacityAnimateControllers = List<AnimationController>(itemTitles.length);
/// 遍歷操做性,初始化每個動畫內容
for (int i = 0; i < itemTitles.length; i++) {
/// 垂直偏移動畫的設定
_itemAnimateControllers[i] = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
_itemCurveAnimations[i] = CurvedAnimation(
parent: _itemAnimateControllers[i],
curve: Curves.ease,
);
/// 垂直偏移量設置爲20
_itemAnimations[i] = Tween(
begin: -20.0,
end: 0.0,
).animate(_itemCurveAnimations[i]) ..addListener(() {
setState(() {
_itemOffset[i] = _itemAnimations[i].value;
});
});
/// 透明度動畫的設定
_itemOpacityAnimateControllers[i] = AnimationController(
duration: Duration(milliseconds: _animateDuration),
vsync: this,
);
_itemOpacityCurveAnimations[i] = CurvedAnimation(
parent: _itemOpacityAnimateControllers[i],
curve: Curves.linear,
);
_itemOpacityAnimations[i] = Tween(
begin: 0.01,
end: 1.0,
).animate(_itemOpacityCurveAnimations[i])
..addListener(() {
setState(() {
_itemOpacity[i] = _itemOpacityAnimations[i].value;
});
});
}
}
/// 操做項動畫的執行
void itemsAnimate(bool forward) {
for (int i = 0; i < _itemAnimateControllers.length; i++) {
/// 每一個操做項依次增長延時,造成連續效果
Future.delayed(Duration(milliseconds: 50 * i), () {
if (forward) {
_itemAnimateControllers[i]?.forward();
_itemOpacityAnimateControllers[i]?.forward();
} else {
_itemAnimateControllers[i]?.reverse();
_itemOpacityAnimateControllers[i]?.reverse();
}
});
}
}
複製代碼
建立操做項的widget
,將動畫值進行綁定:
Widget item(BuildContext context, int index) {
return Stack(
overflow: Overflow.visible,
children: <Widget>[
Positioned(
left: 0.0, right: 0.0,
/// 綁定垂直偏移
top: _itemOffset[index],
child: Opacity(
/// 綁定透明度
opacity: _itemOpacity[index],
child: ...
),
),
],
);
}
複製代碼
最後將動畫初始化放進initState
,動畫執行添加至跳轉動畫。
@override
void initState() {
initItemsAnimation();
/.../
}
Future backDropFilterAnimate(BuildContext context, bool forward) async {
/.../
if (forward) {
/// 以跳轉動畫二分之一的延時執行,效果更佳
Future.delayed(
Duration(milliseconds: _animateDuration ~/ 2),
() { itemsAnimate(true); },
);
} else {
itemsAnimate(false);
}
}
複製代碼
一切就緒,保存就能夠看到精美的動畫效果了~
這個動畫我的耗時大約2小時,在思路很是清晰的狀況下,將動畫效果實現不是一件難事,這樣的動畫其實相對不難,接下來可能會有內容揭開、位置自定義等花式的需求,讓咱們拭目以待~
最後歡迎加入Flutter Candies,一塊兒生產可愛的Flutter小糖果 (QQ羣:181398081)