Flutter 動畫全解析(動畫四要素、動畫組件、隱式動畫組件原理等)

本文通過拆解 Flutter 中動畫的實現方式以及原理來介紹動畫實現的整個過程。

1. 動畫四要素

動畫在各個平臺的實現原理都基本相同,是在一段時間內一系列連續變化畫面的幀構成的。在 Flutter 中,動畫的過程又被量化成一段值區間,我們可以利用這些值設置控件的各個屬性來實現動畫,其內部由四個關鍵的部分來實現這一過程。

1.1 插值器(Tweens)

tweens 可爲動畫提供起始值和結束值。默認情況下,Flutter 中的動畫將任何給定時刻的值映射到介於 0.0 和 1.0 之間的 double 值。 我們可以使用以下 Tween 將其間值的範圍定義爲從 -200.0變爲 0.0:

tween = Tween<double>(begin: -200, end: 0);

我們也可以將值設置爲相應需要改變的對象值,比如將起始值設置爲紅色,結束值設置爲藍色,那麼 tweens 產生的動畫便是由紅漸漸的變成藍色。如下:

colorTween = ColorTween(begin: Colors.red, end: Colors.blue);

1.2 動畫曲線(Animation Curves)

Curves 用來調整動畫過程中隨時間的變化率,默認情況下,動畫以均勻的線性模型變化。讀者可以通過自定義繼承 Curves 的類來定義動畫的變化率,比如設置爲加速、減速或者先加速後減速等曲線模型。Flutter 內部也提供了一系列實現相應變化率的 Curves 對象:

  • linear
  • decelerate
  • ease
  • easeIn
  • easeOut
  • easeInOut
  • fastOutSlowIn
  • bounceIn
  • bounceOut
  • bounceInOut
  • elasticIn
  • elasticOut
  • elasticInOut

動畫曲線模型圖如下:

1.3 Ticker providers

Flutter 中的動畫以屏幕頻繁的重繪而實現,即每秒 60 幀。Ticker 可以被應用在 Flutter 每個對象中,當對象實現了 Ticker 的功能後,每次動畫幀改變便會通知該對象。這裏,開發者們不需要爲對象手動實現 Ticker,flutter 提供了 TickerProvider 類可以幫助我們快速實現該功能。例如,在有狀態控件下使用動畫時,通常需要在 State 對象下混入 TickerProviderStateMixin。

class _MyAnimationState extends State<MyAnimation> 
    with TickerProviderStateMixin {
    
}

1.4 動畫控制器(AnimationController)

Flutter 中動畫的實現還有一個非常重要的類 AnimationController,即動畫控制器。很明顯,我們用它來控制動畫,即動畫的啓動、暫停等。其接受兩個參數,第一個是 vsync,爲 Ticker 對象,其作用是當接受到來自 tweens 和 curves 的新值後通知對應對象,第二個 duration 參數爲動畫持續的時長。

// 混入 SingleTickerProviderStateMixin 使對象實現 Ticker 功能
class _AnimatedContainerState extends State<AnimatedContainer>
        with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 創建 AnimationController 動畫
    _controller = AnimationController(
      // 傳入 Ticker 對象
      vsync: this,
      // 傳入 動畫持續時間
      duration: new Duration(milliseconds: 1000),
    );
    startAnimation();
  }

  Future<void> startAnimation() async {
    // 調用 AnimationController 的 forward 方法啓動動畫
    await _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _controller.value;
      child: //...
    );
  }
}

AnimationController 繼承自 Animation,具有一系列控制動畫的方法,如可用 forward() 方法來啓動動畫,可用 repeat() 方法使動畫重複執行,也可以通過其 value 屬性得到當前值。

1.4.1 Animation

我們可以通過在 CurvedAnimation 傳入 AnimationController 和 Curve 對象創建一個 Animation 對象,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation<double> animation = CurvedAnimation(
  parent: controller,
  curve: Curves.ease,
);

也可以通過調用 tween 的 animate 方法傳入 controller 對象創建 Animation 對象,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Animation 是一個抽象類,其中保存了動畫的過程值(value)和狀態,下面是四種狀態類型。

enum AnimationStatus {
  /// 動畫處於停止狀態
  dismissed,
  /// 動畫從頭到尾執行
  forward,
  /// 動畫從尾到頭執行
  reverse,
  /// 動畫已執行完成
  completed,
}

AnimationController 是它的一個實現類。其內部通過範型機制可實現對各類型對象的動畫,比如 Animation<double>Animation<Color>Animation<Size> 等。其另一個實現類 Curved­Animation,可以用來與 Curves 結合實現各類曲線模型函數的動畫。

Animation 另一個實現方法是調用 tween 對象的 animate 方法傳入 Animation 對象創建另一個 Animation 對象,該方法可通過將使動畫值定義在 tween 區間內,如下:

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

1.4.5 動畫監聽

Animation 對象可以有設置兩種監聽器,分別是幀監聽器和狀態監聽器。使用 addListener() 添加幀監聽器,使用addStatusListener() 添加狀態監聽器。

只要動畫的值發生變化,就會觸發幀監聽器的回調。 通常,我們在其內部調用 setState() 來重建組件來實現動畫效果,如下:

animation = new CurvedAnimation(
        parent: animationController, curve: Curves.elasticOut)
animation.addListener(() => this.setState(() {}))

動畫開始,結束,前進或後退時會觸發 StatusListener 的回調,如下:

animation = new CurvedAnimation(
        parent: animationController, curve: Curves.elasticOut)
animation.addStatusListener((AnimationStatus status) {});

2. 動畫組件

我們已經知道了 Flutter 控制動畫的四大要素,其中涉及的各個概念可以幫助我們設計出各種各樣的動畫效果,但不免也多了一些需要重複編寫的模版代碼,比如,在 Animation 的幀監聽器設置的監聽器回調裏,幾乎所有場景中我們都只是調用 setState(),再比如 State 對象每次都需要我們手動地混入 SingleTickerProviderStateMixin 等等這類情況。Flutter 爲了提高開發者的開發效率,提供了 AnimatedWidget 抽象類來封裝這部分模版代碼,其源碼非常簡單,如下:

abstract class AnimatedWidget extends StatefulWidget {
  /// Creates a widget that rebuilds when the given listenable changes.
  ///
  /// The [listenable] argument is required.
  const AnimatedWidget({
    Key key,
    @required this.listenable
  }) : assert(listenable != null),
       super(key: key);

  /// The [Listenable] to which this widget is listening.
  ///
  /// Commonly an [Animation] or a [ChangeNotifier].
  final Listenable listenable;

  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();
}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

AnimatedWidget 作爲一個抽象類可供我們實現一個我們自己的具體類,其接受一個 Listenable 對象作爲參數,並需要重寫 build 方法。我們上一節中多次提到的 Animation 繼承自 Listenable。下面的這個這個組件就是我自己實現的動畫組件:??

class Sun extends AnimatedWidget {
  Sun({Key key, Animation<Color> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<Color> animation = listenable;
    var maxWidth = MediaQuery.of(context).size.width;
    var margin = (maxWidth * .3) / 3;

    return new AspectRatio(
        aspectRatio: 1.0,
        child: new Container(
            margin: EdgeInsets.symmetric(horizontal: margin),
            constraints: BoxConstraints(
              maxWidth: maxWidth,
            ),
            decoration: new BoxDecoration(
              shape: BoxShape.circle,
              color: animation.value,
            )));
  }
}

我們可以通過傳入已經定義好的 Animation 對象來使用該組件:??

class AnimateWidgetState extends State<AnimateWidget> {
  AnimationController _animationController;
  ColorTween _colorTween;
  ...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: 
          Column(
        children: <Widget>[
          Sun(animation: _colorTween.animate(_animationController)),
        ],
      ),
    );
  }
}

這樣我們就封裝了自己的一個動畫組件,另外,Flutter 內部爲我們提供了多個已經封裝好的動畫組件,利用好這些組件可以大大地提高我們的開發效率:

  • SlideTransition
  • ScaleTransition
  • RotationTransition
  • SizeTransition

3. 隱式動畫組件

利用動畫組件我們已經可以方便地封裝出一系列控件動畫了,但是這種實現方式均需要我們自己提供 Animation 對象,然後通過提供的接口方法來啓動我們的動畫,控件的屬性由 Animation 對象提供並在動畫過程中改變而達到動畫的效果。爲了使動畫使用起來更加方便,Flutter 幫助了開發者從另一個角度以更簡單的方式實現了動畫效果——隱式動畫組件(ImplicitlyAnimatedWidget)。

通過隱式動畫組件,我們不需要手動實現插值器、曲線等對象,開發者甚至也不需要使用 AnimationController 來啓動動畫,它的實現方式更貼近對組件本身的操作,我們可以直接通過 setState() 的方法改變隱式動畫組件的屬性值,其內部自行爲我們實現動畫過程的過渡效果,即隱藏了所有動畫實現的細節。Flutter 內部爲我們提供了多個實用的隱式動畫組件,我們本節分別介紹 AnimatedContainer 和 AnimatedOpacity 這兩個最常用的隱式動畫組件。

3.1 AnimatedContainer

AnimatedContainer 是我們最常使用到的隱式動畫組件之一,從名字可以看出這個控件是以動畫形式而成的 Contianer 控件,它們都是頁面中渲染一個空的容器並且使用方法也非常相似。我們可以用下面的方式使用 Contianer 控件:

var height = 40.0  
...
    
Container(
    width: 60.0,
    height: height,
    color: Color(0xff14ff65),
  ),

上面的代碼中,我們將 Container 的高度設置爲 height 變量,即爲 40.0,當我們使用一個 Button 按鈕觸發改變 height 值的事件並且重繪界面時,Container 的高度會隨之改變:

onPressed: (){
  setState(() {
    height = 320.0;
  });
},

但這種變化很明顯僅是屬性的改變並不是一個平滑的過渡動畫,然而同樣的事件發生在 AnimatedContainer 控件上,便會有一個漸變的效果:

AnimatedContainer(
  duration: Duration(seconds: 5),
  width: 60.0,
  height: height,
  color: Color(0xff14ff65),
)

使用 AnimatedContainer 後,我們再次觸發 height 變量改變後,頁面中的 AnimatedContainer 便會平滑的過渡到相應的高度,其 duration 屬性用於設置動畫過渡的時間,這裏,我們設置爲 5 秒??。

我們可以用相同的方式爲 Container 的 Color、width 等各種屬性設置動畫,同時也可以通過爲其設置 alignment 屬性來設置其內部子控件的位置。

3.2 AnimatedOpacity

在 Flutter 中,另一種常用的動畫是控件透明度的過渡動畫,其對應的隱式動畫組件爲 AnimatedOpacity。它的用法與 Opacity 相似,內部持有的 opacity 屬性可以設置爲 0.0~1.0 中的任意浮點數,分別對應完全透明與完全不透明,使用下面的方式,我們便可以設置了一個半透明的 Opacity 控件:

Opacity(
    opacity: 0.5,
    child: Text("hello"),
)

我們以相同的方法使用 AnimatedOpacity:

double opacity = 1.0;
...
AnimatedOpacity(
    opacity: opacity,
    duration: Duration(seconds: 1),
    child: Text("hello"),
)

它也接受 duration 屬性來設置過渡時間,通過改變 opacity 變量的值可以實現透明度變化的動畫效果:

setState(() {
    opacity = 0.0;
});

3.3 隱式動畫原理簡析

我們已經在本書前部分介紹了 Flutter 中的三棵重要的樹及它們在組件渲染中的作用了。在元素樹中,每個 Element 對象持有控件樹中 Widget 組件的狀態信息,這裏我們將它稱爲 State 對象,Widget 刷新重建時,Element 會對比自己所對應 Widget 是否更新而做出相應屏幕渲染上的改變。

在各個隱式動畫組件中,其動畫信息便儲存在 Element 所持有的 State 對象中,Widget 每次刷新都會引起 Element 對其重新引用,當對應的 Widget 類型改變則其 Element 會連帶 State 對象自然而然的需要重新渲染,然而當 Widget 類型不變,則 Element 不需要重建,只需要改變 State 對象儲存的動畫信息即可。這樣一種連續更新屬性的過程便實現了更爲我們所方便使用的隱式動畫。

3.4 實現自定義隱式動畫組件

實現自定義的隱式動畫組件,我們需要使用到兩個類:ImplicitlyAnimatedWidget 和 AnimatedWidgetBaseState。??

ImplicitlyAnimatedWidget 是所有隱式動畫組件的父類,繼承自 StatefulWidget,並且僅需要接受動畫曲線 curve 與動畫過渡時長 duration 兩個參數:

const ImplicitlyAnimatedWidget({
    Key key,
    this.curve = Curves.linear,
    @required this.duration
  }) 

在我們自定義的隱式動畫組件可以擴充他的參數類型滿足我們的需求。

AnimatedWidgetBaseState 即 ImplicitlyAnimatedWidget 這個有狀態組件所對應的 State 對象類,我們自定義的隱式動畫組件所對應的 State 也必須繼承該類,其內部需要重寫 forEachTween 方法。

下面就是我自己定義的隱式動畫組件:

class MyAnimatedWidget extends ImplicitlyAnimatedWidget {
  MyAnimatedWidget({
    Key key,
    this.param, //導致動畫的參數
    Curve curve = Curves.linear,
    @required Duration duration,
  }) :super(key: key, curve: curve, duration: duration);
  final double param;
  
  @override
  _MyAnimatedWidgetState createState() => _MyAnimatedWidgetState();
}

class _MyAnimatedWidgetState extends AnimatedWidgetBaseState<MyAnimatedWidget> {
  Tween<double> _param; // State 內部保存的當前狀態信息,類型爲 Tween
  
  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _param = visitor(_param, widget.param, (value) => Tween<double>(begin: value));
  }
  
  @override
  Widget build(BuildContext context) {
    //return a widget built on a parameter
  }
}

上面代碼中,我們在父類的基礎之上拓展了 param 參數,其是我們在動畫過程中需要關注的動畫屬性值。我們還需要重點關注 _MyAnimatedWidgetState 類中 �forEachTween 方法,它是隱式動畫實現的核心方法,其用於每次更新組件的動畫屬性,接受一個 TweenVisitor 對象 visitor 作爲參數。visitor 同時接受是那個參數,第一個爲一個插值器對象 Tween<T>,其是應用在屬性中的插值器當前補間值,第二個參數爲一個 T 類型的值,即新的目標屬性值,第三個參數爲一個回調函數,用於配置給定的 value 值作爲新的插值器開始值。TweenVisitor<T> 函數返回一個 Tween<T> 對象,我們將其賦值給組件中當前的插值器對象作爲下次調用 forEachTween 方法時的當前值。

4. 其他

筆者水平有限,如果文中有錯誤的地方,請留言指正。

歡迎一起交流學習,聯繫方式:

我的博客原文:https://meandni.com/2019/07/01/c0f2/

Github:https://github.com/MeandNi

微信:yangjk128

5. 參考

Flutter Doc

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章