相關文章
- Flutter 實現底部擴散模糊動畫(一)跳轉頁面
- Flutter 實現底部擴散模糊動畫(二)頁面互動
前言
在上一期,我們已經完成了點開動畫的編寫和執行,如果有仔細看完的小夥伴會發現,其中的動畫效果不止擴散這麼簡單,本篇就來繼續研究其餘的動畫互動。
簡介
作為一個炫(pin)酷(ru)的頁面,頁面中的互動也非常的重要。在本篇,我將進一步說明頁面內各個位置的互動細節,從而帶著各位做一個不將就的強迫症~
效果圖:
完整demo及元件已上傳至專案,走過路過留個star~
- ripple_backdrop_animate_route
- OpenJMU/lib/pages/home/AddButtonPage.dart
- OpenJMU/lib/pages/MainPage.dart
互動要素
頁面中的互動主要包含三個觸發位置:
- 點選空白的模糊處,頁面會執行退出和退出動畫;
- 點選頁面上的返回或關閉按鈕,頁面會執行退出和退出動畫;
- 元素漸顯並帶有其他效果。
接下來將逐點說明如何實現。
實現過程
攔截返回操作
我們知道在Flutter中,頁面要返回時,會執行Navigator.maybePop
的方法,使頁面返回。為了攔截路由pop
,Flutter提供了WillPopScope
來攔截返回行為,我們只需要註冊onWillPop
方法,就可以在pop
前執行程式碼。
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,
),
),
);
}
複製程式碼
如此我們就輕鬆愉快地攔截了路由~
退出動畫
思考退出動畫和跳轉動畫的關係,我們立馬就可以想到,跳轉和退出的動畫是相反的,也就是說,逆向執行跳轉的動畫,就能得到一個退出動畫。
這時我們來回顧一下上一期的跳轉動畫:
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
來判斷方向,可以使未完全覆蓋的動畫從停止處反向執行,不會造成閃爍的情況。
至此,跳轉和退出動畫已經完美完成。
"X" & 空白處返回
根據效果圖,在頁面的底部,會提供一個帶有旋轉動畫返回按鈕,點選可以返回。
由於我的頁面時點選加號觸發的,所以這裡我引入了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)