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 的定義:

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

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

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

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

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

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

Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
  return FadeTransition(
    opacity: animation,
    child: child,
  );
}

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

#例子

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

import 'package:flutter/material.dart';


class AnimatedSwitcherCounterRoute extends StatefulWidget {
   const AnimatedSwitcherCounterRoute({Key key}) : super(key: key);


   @override
   _AnimatedSwitcherCounterRouteState createState() => _AnimatedSwitcherCounterRouteState();
 }


 class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
   int _count = 0;


   @override
   Widget build(BuildContext context) {
     return Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           AnimatedSwitcher(
             duration: const Duration(milliseconds: 500),
             transitionBuilder: (Widget child, Animation<double> animation) {
               //執(zhí)行縮放動畫
               return ScaleTransition(child: child, scale: animation);
             },
             child: Text(
               '$_count',
               //顯示指定key,不同的key會被認(rèn)為是不同的Text,這樣才能執(zhí)行動畫
               key: ValueKey<int>(_count),
               style: Theme.of(context).textTheme.headline4,
             ),
           ),
           RaisedButton(
             child: const Text('+1',),
             onPressed: () {
               setState(() {
                 _count += 1;
               });
             },
           ),
         ],
       ),
     );
   }
 }

運行示例代碼,當(dāng)點擊“+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的使用方式我們可以看到,當(dāng) child 發(fā)生變化時(子 widget 的key和類型同時相等則認(rèn)為發(fā)生變化),則重新會重新執(zhí)行build,然后動畫開始執(zhí)行。我們可以通過繼承 StatefulWidget來實現(xiàn)AnimatedSwitcher,具體做法是在didUpdateWidget 回調(diào)中判斷其新舊 child 是否發(fā)生變化,如果發(fā)生變化,則對舊 child 執(zhí)行反向退場(reverse)動畫,對新 child 執(zhí)行正向(forward)入場動畫即可。下面是AnimatedSwitcher實現(xiàn)的部分核心偽代碼:

Widget _widget; //
void didUpdateWidget(AnimatedSwitcher oldWidget) {
  super.didUpdateWidget(oldWidget);
  // 檢查新舊child是否發(fā)生變化(key和類型同時相等則返回true,認(rèn)為沒變化)
  if (Widget.canUpdate(widget.child, oldWidget.child)) {
    // child沒變化,...
  } else {
    //child發(fā)生了變化,構(gòu)建一個Stack來分別給新舊child執(zhí)行動畫
   _widget= Stack(
      alignment: Alignment.center,
      children:[
        //舊child應(yīng)用FadeTransition
        FadeTransition(
         opacity: _controllerOldAnimation,
         child : oldWidget.child,
        ),
        //新child應(yīng)用FadeTransition
        FadeTransition(
         opacity: _controllerNewAnimation,
         child : widget.child,
        ),
      ]
    );
    // 給舊child執(zhí)行反向退場動畫
    _controllerOldAnimation.reverse();
    //給新child執(zhí)行正向入場動畫
    _controllerNewAnimation.forward();
  }
}


//build方法
Widget build(BuildContext context){
  return _widget;
}

上面?zhèn)未a展示了AnimatedSwitcher實現(xiàn)的核心邏輯,當(dāng)然AnimatedSwitcher真正的實現(xiàn)比這個復(fù)雜,它可以自定義進(jì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è)平移進(jìn)入。如果要用 AnimatedSwitcher 的話,我們很快就會發(fā)現(xiàn)一個問題:做不到!我們可能會寫出下面的代碼:

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return SlideTransition(
       child: child,
       position: tween.animate(animation),
    );
  },
  ...//省略
)

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

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

class MySlideTransition extends AnimatedWidget {
  MySlideTransition({
    Key key,
    @required Animation<Offset> position,
    this.transformHitTests = true,
    this.child,
  })
      : assert(position != null),
        super(key: key, listenable: position) ;


  Animation<Offset> get position => listenable;
  final bool transformHitTests;
  final Widget child;


  @override
  Widget build(BuildContext context) {
    Offset offset=position.value;
    //動畫反向執(zhí)行時,調(diào)整x偏移,實現(xiàn)“從左邊滑出隱藏”
    if (position.status == AnimationStatus.reverse) {
         offset = Offset(-offset.dx, offset.dy);
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

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

AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  transitionBuilder: (Widget child, Animation<double> animation) {
    var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
     return MySlideTransition(
              child: child,
              position: tween.animate(animation),
              );
  },
  ...//省略
)

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

圖9-6

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

#SlideTransitionX

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

class SlideTransitionX extends AnimatedWidget {
  SlideTransitionX({
    Key key,
    @required Animation<double> position,
    this.transformHitTests = true,
    this.direction = AxisDirection.down,
    this.child,
  })
      : assert(position != null),
        super(key: key, listenable: position) {
    // 偏移在內(nèi)部處理      
    switch (direction) {
      case AxisDirection.up:
        _tween = Tween(begin: Offset(0, 1), end: Offset(0, 0));
        break;
      case AxisDirection.right:
        _tween = Tween(begin: Offset(-1, 0), end: Offset(0, 0));
        break;
      case AxisDirection.down:
        _tween = Tween(begin: Offset(0, -1), end: Offset(0, 0));
        break;
      case AxisDirection.left:
        _tween = Tween(begin: Offset(1, 0), end: Offset(0, 0));
        break;
    }
  }




  Animation<double> get position => listenable;


  final bool transformHitTests;


  final Widget child;


  //退場(出)方向
  final AxisDirection direction;


  Tween<Offset> _tween;


  @override
  Widget build(BuildContext context) {
    Offset offset = _tween.evaluate(position);
    if (position.status == AnimationStatus.reverse) {
      switch (direction) {
        case AxisDirection.up:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.right:
          offset = Offset(-offset.dx, offset.dy);
          break;
        case AxisDirection.down:
          offset = Offset(offset.dx, -offset.dy);
          break;
        case AxisDirection.left:
          offset = Offset(-offset.dx, offset.dy);
          break;
      }
    }
    return FractionalTranslation(
      translation: offset,
      transformHitTests: transformHitTests,
      child: child,
    );
  }
}

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

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

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

圖9-7

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

#總結(jié)

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號