Flutter實戰(zhàn) 通用“動畫切換”組件(AnimatedSwitcher)

2021-03-08 18:01 更新

實際開發(fā)中,我們經(jīng)常會遇到切換 UI 元素的場景,比如 Tab 切換、路由切換。為了增強用戶體驗,通常在切換時都會指定一個動畫,以使切換過程顯得平滑。Flutter SDK 組件庫中已經(jīng)提供了一些常用的切換組件,如PageView、TabView等,但是,這些組件并不能覆蓋全部的需求場景,為此,F(xiàn)lutter SDK 中提供了一個AnimatedSwitcher組件,它定義了一種通用的UI切換抽象。

#9.6.1 AnimatedSwitcher

AnimatedSwitcher 可以同時對其新、舊子元素添加顯示、隱藏動畫。也就是說在AnimatedSwitcher的子元素發(fā)生變化時,會對其舊元素和新元素,我們先看看AnimatedSwitcher 的定義:

  1. const AnimatedSwitcher({
  2. Key key,
  3. this.child,
  4. @required this.duration, // 新child顯示動畫時長
  5. this.reverseDuration,// 舊child隱藏的動畫時長
  6. this.switchInCurve = Curves.linear, // 新child顯示的動畫曲線
  7. this.switchOutCurve = Curves.linear,// 舊child隱藏的動畫曲線
  8. this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 動畫構(gòu)建器
  9. this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局構(gòu)建器
  10. })

AnimatedSwitcher的 child 發(fā)生變化時(類型或Key不同),舊 child 會執(zhí)行隱藏動畫,新child會執(zhí)行執(zhí)行顯示動畫。究竟執(zhí)行何種動畫效果則由transitionBuilder參數(shù)決定,該參數(shù)接受一個AnimatedSwitcherTransitionBuilder類型的builder,定義如下:

  1. typedef AnimatedSwitcherTransitionBuilder =
  2. Widget Function(Widget child, Animation<double> animation);

builderAnimatedSwitcher的 child 切換時會分別對新、舊child綁定動畫:

  1. 對舊 child,綁定的動畫會反向執(zhí)行(reverse)
  2. 對新 child,綁定的動畫會正向指向(forward)

這樣一下,便實現(xiàn)了對新、舊 child 的動畫綁定。AnimatedSwitcher的默認值是AnimatedSwitcher.defaultTransitionBuilder

  1. Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
  2. return FadeTransition(
  3. opacity: animation,
  4. child: child,
  5. );
  6. }

可以看到,返回了FadeTransition對象,也就是說默認情況,AnimatedSwitcher會對新舊child執(zhí)行“漸隱”和“漸顯”動畫。

#例子

下面我們看一個列子:實現(xiàn)一個計數(shù)器,然后再每一次自增的過程中,舊數(shù)字執(zhí)行縮小動畫隱藏,新數(shù)字執(zhí)行放大動畫顯示,代碼如下:

  1. import 'package:flutter/material.dart';
  2. class AnimatedSwitcherCounterRoute extends StatefulWidget {
  3. const AnimatedSwitcherCounterRoute({Key key}) : super(key: key);
  4. @override
  5. _AnimatedSwitcherCounterRouteState createState() => _AnimatedSwitcherCounterRouteState();
  6. }
  7. class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
  8. int _count = 0;
  9. @override
  10. Widget build(BuildContext context) {
  11. return Center(
  12. child: Column(
  13. mainAxisAlignment: MainAxisAlignment.center,
  14. children: <Widget>[
  15. AnimatedSwitcher(
  16. duration: const Duration(milliseconds: 500),
  17. transitionBuilder: (Widget child, Animation<double> animation) {
  18. //執(zhí)行縮放動畫
  19. return ScaleTransition(child: child, scale: animation);
  20. },
  21. child: Text(
  22. '$_count',
  23. //顯示指定key,不同的key會被認為是不同的Text,這樣才能執(zhí)行動畫
  24. key: ValueKey<int>(_count),
  25. style: Theme.of(context).textTheme.headline4,
  26. ),
  27. ),
  28. RaisedButton(
  29. child: const Text('+1',),
  30. onPressed: () {
  31. setState(() {
  32. _count += 1;
  33. });
  34. },
  35. ),
  36. ],
  37. ),
  38. );
  39. }
  40. }

運行示例代碼,當點擊“+1”按鈕時,原先的數(shù)字會逐漸縮小直至隱藏,而新數(shù)字會逐漸放大,我截取了動畫執(zhí)行過程的一幀,如圖9-5所示:

上圖是第一次點擊“+1”按鈕后切換動畫的一幀,此時“0”正在逐漸縮小,而“1”正在“0”的中間,正在逐漸放大。

注意:AnimatedSwitcher的新舊child,如果類型相同,則Key必須不相等。

#AnimatedSwitcher實現(xiàn)原理

實際上,AnimatedSwitcher的實現(xiàn)原理是比較簡單的,我們根據(jù)AnimatedSwitcher的使用方式也可以猜個大概。要想實現(xiàn)新舊 child 切換動畫,只需要明確兩個問題:動畫執(zhí)行的時機是和如何對新舊 child 執(zhí)行動畫。從AnimatedSwitcher的使用方式我們可以看到,當 child 發(fā)生變化時(子 widget 的key和類型同時相等則認為發(fā)生變化),則重新會重新執(zhí)行build,然后動畫開始執(zhí)行。我們可以通過繼承 StatefulWidget來實現(xiàn)AnimatedSwitcher,具體做法是在didUpdateWidget 回調(diào)中判斷其新舊 child 是否發(fā)生變化,如果發(fā)生變化,則對舊 child 執(zhí)行反向退場(reverse)動畫,對新 child 執(zhí)行正向(forward)入場動畫即可。下面是AnimatedSwitcher實現(xiàn)的部分核心偽代碼:

  1. Widget _widget; //
  2. void didUpdateWidget(AnimatedSwitcher oldWidget) {
  3. super.didUpdateWidget(oldWidget);
  4. // 檢查新舊child是否發(fā)生變化(key和類型同時相等則返回true,認為沒變化)
  5. if (Widget.canUpdate(widget.child, oldWidget.child)) {
  6. // child沒變化,...
  7. } else {
  8. //child發(fā)生了變化,構(gòu)建一個Stack來分別給新舊child執(zhí)行動畫
  9. _widget= Stack(
  10. alignment: Alignment.center,
  11. children:[
  12. //舊child應(yīng)用FadeTransition
  13. FadeTransition(
  14. opacity: _controllerOldAnimation,
  15. child : oldWidget.child,
  16. ),
  17. //新child應(yīng)用FadeTransition
  18. FadeTransition(
  19. opacity: _controllerNewAnimation,
  20. child : widget.child,
  21. ),
  22. ]
  23. );
  24. // 給舊child執(zhí)行反向退場動畫
  25. _controllerOldAnimation.reverse();
  26. //給新child執(zhí)行正向入場動畫
  27. _controllerNewAnimation.forward();
  28. }
  29. }
  30. //build方法
  31. Widget build(BuildContext context){
  32. return _widget;
  33. }

上面?zhèn)未a展示了AnimatedSwitcher實現(xiàn)的核心邏輯,當然AnimatedSwitcher真正的實現(xiàn)比這個復雜,它可以自定義進退場過渡動畫以及執(zhí)行動畫時的布局等。在此,我們刪繁就簡,通過偽代碼形式讓讀者能夠清楚看到主要的實現(xiàn)思路,具體的實現(xiàn)讀者可以參考AnimatedSwitcher源碼。

另外,F(xiàn)lutter SDK 中還提供了一個AnimatedCrossFade組件,它也可以切換兩個子元素,切換過程執(zhí)行漸隱漸顯的動畫,和AnimatedSwitcher不同的是AnimatedCrossFade是針對兩個子元素,而AnimatedSwitcher是在一個子元素的新舊值之間切換。AnimatedCrossFade實現(xiàn)原理比較簡單,也有和AnimatedSwitcher類似的地方,因此不再贅述,讀者有興趣可以查看其源碼。

#9.6.2 AnimatedSwitcher高級用法

假設(shè)現(xiàn)在我們想實現(xiàn)一個類似路由平移切換的動畫:舊頁面屏幕中向左側(cè)平移退出,新頁面重屏幕右側(cè)平移進入。如果要用 AnimatedSwitcher 的話,我們很快就會發(fā)現(xiàn)一個問題:做不到!我們可能會寫出下面的代碼:

  1. AnimatedSwitcher(
  2. duration: Duration(milliseconds: 200),
  3. transitionBuilder: (Widget child, Animation<double> animation) {
  4. var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
  5. return SlideTransition(
  6. child: child,
  7. position: tween.animate(animation),
  8. );
  9. },
  10. ...//省略
  11. )

上面的代碼有什么問題呢?我們前面說過在AnimatedSwitcher的 child 切換時會分別對新 child 執(zhí)行正向動畫(forward),而對舊 child 執(zhí)行反向動畫(reverse),所以真正的效果便是:新 child 確實從屏幕右側(cè)平移進入了,但舊 child 卻會從屏幕右側(cè)(而不是左側(cè))退出。其實也很容易理解,因為在沒有特殊處理的情況下,同一個動畫的正向和逆向正好是相反(對稱)的。

那么問題來了,難道就不能使用AnimatedSwitcher了?答案當然是否定的!仔細想想這個問題,究其原因,就是因為同一個Animation正向(forward)和反向(reverse)是對稱的。所以如果我們可以打破這種對稱性,那么便可以實現(xiàn)這個功能了,下面我們來封裝一個MySlideTransition,它與SlideTransition唯一的不同就是對動畫的反向執(zhí)行進行了定制(從左邊滑出隱藏),代碼如下:

  1. class MySlideTransition extends AnimatedWidget {
  2. MySlideTransition({
  3. Key key,
  4. @required Animation<Offset> position,
  5. this.transformHitTests = true,
  6. this.child,
  7. })
  8. : assert(position != null),
  9. super(key: key, listenable: position) ;
  10. Animation<Offset> get position => listenable;
  11. final bool transformHitTests;
  12. final Widget child;
  13. @override
  14. Widget build(BuildContext context) {
  15. Offset offset=position.value;
  16. //動畫反向執(zhí)行時,調(diào)整x偏移,實現(xiàn)“從左邊滑出隱藏”
  17. if (position.status == AnimationStatus.reverse) {
  18. offset = Offset(-offset.dx, offset.dy);
  19. }
  20. return FractionalTranslation(
  21. translation: offset,
  22. transformHitTests: transformHitTests,
  23. child: child,
  24. );
  25. }
  26. }

調(diào)用時,將SlideTransition替換成MySlideTransition即可:

  1. AnimatedSwitcher(
  2. duration: Duration(milliseconds: 200),
  3. transitionBuilder: (Widget child, Animation<double> animation) {
  4. var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
  5. return MySlideTransition(
  6. child: child,
  7. position: tween.animate(animation),
  8. );
  9. },
  10. ...//省略
  11. )

運行后,我截取動畫執(zhí)行過程中的一幀,如圖9-6所示:

圖9-6

上圖中“0”從左側(cè)滑出,而“1”從右側(cè)滑入??梢钥吹?,我們通過這種巧妙的方式實現(xiàn)了類似路由進場切換的動畫,實際上 Flutter 路由切換也正是通過AnimatedSwitcher來實現(xiàn)的。

#SlideTransitionX

上面的示例我們實現(xiàn)了“左出右入”的動畫,那如果要實現(xiàn)“右入左出”、“上入下出”或者 “下入上出”怎么辦?當然,我們可以分別修改上面的代碼,但是這樣每種動畫都得單獨定義一個“Transition”,這很麻煩。本節(jié)將封裝一個通用的SlideTransitionX 來實現(xiàn)這種“出入滑動動畫”,代碼如下:

  1. class SlideTransitionX extends AnimatedWidget {
  2. SlideTransitionX({
  3. Key key,
  4. @required Animation<double> position,
  5. this.transformHitTests = true,
  6. this.direction = AxisDirection.down,
  7. this.child,
  8. })
  9. : assert(position != null),
  10. super(key: key, listenable: position) {
  11. // 偏移在內(nèi)部處理
  12. switch (direction) {
  13. case AxisDirection.up:
  14. _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
  15. break;
  16. case AxisDirection.right:
  17. _tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
  18. break;
  19. case AxisDirection.down:
  20. _tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
  21. break;
  22. case AxisDirection.left:
  23. _tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
  24. break;
  25. }
  26. }
  27. Animation<double> get position => listenable;
  28. final bool transformHitTests;
  29. final Widget child;
  30. //退場(出)方向
  31. final AxisDirection direction;
  32. Tween<Offset> _tween;
  33. @override
  34. Widget build(BuildContext context) {
  35. Offset offset = _tween.evaluate(position);
  36. if (position.status == AnimationStatus.reverse) {
  37. switch (direction) {
  38. case AxisDirection.up:
  39. offset = Offset(offset.dx, -offset.dy);
  40. break;
  41. case AxisDirection.right:
  42. offset = Offset(-offset.dx, offset.dy);
  43. break;
  44. case AxisDirection.down:
  45. offset = Offset(offset.dx, -offset.dy);
  46. break;
  47. case AxisDirection.left:
  48. offset = Offset(-offset.dx, offset.dy);
  49. break;
  50. }
  51. }
  52. return FractionalTranslation(
  53. translation: offset,
  54. transformHitTests: transformHitTests,
  55. child: child,
  56. );
  57. }
  58. }

現(xiàn)在如果我們想實現(xiàn)各種“滑動出入動畫”便非常容易,只需給direction傳遞不同的方向值即可,比如要實現(xiàn)“上入下出”,則:

  1. AnimatedSwitcher(
  2. duration: Duration(milliseconds: 200),
  3. transitionBuilder: (Widget child, Animation<double> animation) {
  4. var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
  5. return SlideTransitionX(
  6. child: child,
  7. direction: AxisDirection.down, //上入下出
  8. position: animation,
  9. );
  10. },
  11. ...//省略其余代碼
  12. )

運行后,我截取動畫執(zhí)行過程中的一幀,如圖9-7所示:

圖9-7

上圖中“1”從底部滑出,而“2”從頂部滑入。讀者可以嘗試給SlideTransitionXdirection取不同的值來查看運行效果。

#總結(jié)

本節(jié)我們學習了AnimatedSwitcher的詳細用法,同時也介紹了打破AnimatedSwitcher動畫對稱性的方法。我們可以發(fā)現(xiàn):在需要切換新舊 UI 元素的場景,AnimatedSwitcher將十分實用。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號