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

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

1. 動畫四要素

動畫在各個平臺的實現(xiàn)原理都基本相同,是在一段時間內(nèi)一系列連續(xù)變化畫面的幀構(gòu)成的。在 Flutter 中,動畫的過程又被量化成一段值區(qū)間,我們可以利用這些值設(shè)置控件的各個屬性來實現(xiàn)動畫,其內(nèi)部由四個關(guān)鍵的部分來實現(xiàn)這一過程。

1.1 插值器(Tweens)

tweens 可為動畫提供起始值和結(jié)束值。默認(rèn)情況下,F(xiàn)lutter 中的動畫將任何給定時刻的值映射到介于 0.0 和 1.0 之間的 double 值。 我們可以使用以下 Tween 將其間值的范圍定義為從 -200.0變?yōu)?0.0:

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

我們也可以將值設(shè)置為相應(yīng)需要改變的對象值,比如將起始值設(shè)置為紅色,結(jié)束值設(shè)置為藍(lán)色,那么 tweens 產(chǎn)生的動畫便是由紅漸漸的變成藍(lán)色。如下:

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

1.2 動畫曲線(Animation Curves)

Curves 用來調(diào)整動畫過程中隨時間的變化率,默認(rèn)情況下,動畫以均勻的線性模型變化。讀者可以通過自定義繼承 Curves 的類來定義動畫的變化率,比如設(shè)置為加速、減速或者先加速后減速等曲線模型。Flutter 內(nèi)部也提供了一系列實現(xiàn)相應(yīng)變化率的 Curves 對象:

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

動畫曲線模型圖如下:

curve_linear.gif
curve_ease_in.gif
curve_bounce_in.gif

1.3 Ticker providers

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

class _MyAnimationState extends State<MyAnimation> 
    with TickerProviderStateMixin {
    
}

1.4 動畫控制器(AnimationController)

Flutter 中動畫的實現(xiàn)還有一個非常重要的類 AnimationController,即動畫控制器。很明顯,我們用它來控制動畫,即動畫的啟動、暫停等。其接受兩個參數(shù),第一個是 vsync,為 Ticker 對象,其作用是當(dāng)接受到來自 tweens 和 curves 的新值后通知對應(yīng)對象,第二個 duration 參數(shù)為動畫持續(xù)的時長。

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

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

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

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

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

1.4.1 Animation

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

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

也可以通過調(diào)用 tween 的 animate 方法傳入 controller 對象創(chuàng)建 Animation 對象,如下:

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

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

enum AnimationStatus {
  /// 動畫處于停止?fàn)顟B(tài)
  dismissed,
  /// 動畫從頭到尾執(zhí)行
  forward,
  /// 動畫從尾到頭執(zhí)行
  reverse,
  /// 動畫已執(zhí)行完成
  completed,
}

AnimationController 是它的一個實現(xiàn)類。其內(nèi)部通過范型機(jī)制可實現(xiàn)對各類型對象的動畫,比如 Animation<double>Animation<Color>、Animation<Size> 等。其另一個實現(xiàn)類 Curved-Animation,可以用來與 Curves 結(jié)合實現(xiàn)各類曲線模型函數(shù)的動畫。

Animation 另一個實現(xiàn)方法是調(diào)用 tween 對象的 animate 方法傳入 Animation 對象創(chuàng)建另一個 Animation 對象,該方法可通過將使動畫值定義在 tween 區(qū)間內(nèi),如下:

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 動畫監(jiān)聽

Animation 對象可以有設(shè)置兩種監(jiān)聽器,分別是幀監(jiān)聽器和狀態(tài)監(jiān)聽器。使用 addListener() 添加幀監(jiān)聽器,使用addStatusListener() 添加狀態(tài)監(jiān)聽器。

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

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

動畫開始,結(jié)束,前進(jìn)或后退時會觸發(fā) StatusListener 的回調(diào),如下:

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

2. 動畫組件

我們已經(jīng)知道了 Flutter 控制動畫的四大要素,其中涉及的各個概念可以幫助我們設(shè)計出各種各樣的動畫效果,但不免也多了一些需要重復(fù)編寫的模版代碼,比如,在 Animation 的幀監(jiān)聽器設(shè)置的監(jiān)聽器回調(diào)里,幾乎所有場景中我們都只是調(diào)用 setState(),再比如 State 對象每次都需要我們手動地混入 SingleTickerProviderStateMixin 等等這類情況。Flutter 為了提高開發(fā)者的開發(fā)效率,提供了 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 作為一個抽象類可供我們實現(xiàn)一個我們自己的具體類,其接受一個 Listenable 對象作為參數(shù),并需要重寫 build 方法。我們上一節(jié)中多次提到的 Animation 繼承自 Listenable。下面的這個這個組件就是我自己實現(xiàn)的動畫組件:??

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,
            )));
  }
}

我們可以通過傳入已經(jīng)定義好的 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)),
        ],
      ),
    );
  }
}

這樣我們就封裝了自己的一個動畫組件,另外,F(xiàn)lutter 內(nèi)部為我們提供了多個已經(jīng)封裝好的動畫組件,利用好這些組件可以大大地提高我們的開發(fā)效率:

  • SlideTransition
  • ScaleTransition
  • RotationTransition
  • SizeTransition

3. 隱式動畫組件

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

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

3.1 AnimatedContainer

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

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

上面的代碼中,我們將 Container 的高度設(shè)置為 height 變量,即為 40.0,當(dāng)我們使用一個 Button 按鈕觸發(fā)改變 height 值的事件并且重繪界面時,Container 的高度會隨之改變:

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

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

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

使用 AnimatedContainer 后,我們再次觸發(fā) height 變量改變后,頁面中的 AnimatedContainer 便會平滑的過渡到相應(yīng)的高度,其 duration 屬性用于設(shè)置動畫過渡的時間,這里,我們設(shè)置為 5 秒??。

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

3.2 AnimatedOpacity

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

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

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

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

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

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

3.3 隱式動畫原理簡析

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

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

3.4 實現(xiàn)自定義隱式動畫組件

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

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

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

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

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

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

class MyAnimatedWidget extends ImplicitlyAnimatedWidget {
  MyAnimatedWidget({
    Key key,
    this.param, //導(dǎo)致動畫的參數(shù)
    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 內(nèi)部保存的當(dāng)前狀態(tài)信息,類型為 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
  }
}

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

4. 其他

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

歡迎一起交流學(xué)習(xí),聯(lián)系方式:

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

Github:https://github.com/MeandNi

微信:yangjk128

5. 參考

Flutter Doc

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容