Flutter 掌握動畫開發

曉峰殘月發表於2019-09-25

主本文主要說明動畫的基本原理和簡單的動畫的例項,如有不當之處敬請指正。

閱讀本文大約需要 6 分鐘

背景

給UI介面設計合理的動畫,可以讓使用者覺得更加流暢、直觀,提高使用者的互動使用感受,改善使用者體驗。

在 Flutter 中動畫分為兩類:基於補間 (Tween) 的和基於物理 (Physics) 的;

補間動畫是介於兩者之間的簡稱,在補間動畫中定義起點和終點、時間點以及定義時間變化和速度的曲線,然後由系統計算如何從開始點到結束點。

物理動畫是運動被模擬為與真實世界的行為相似,比如拋一件物體,它落在什麼地方取決於這個物體的重量,丟擲去的速度以及這個物體與地面的高度,類似數學中的拋物線運動軌跡。

介紹

在 Flutter 中想要實現動畫效果離不開幾個核心的角色:Animation(動畫物件),AnimationController(動畫控制器),Tweens(插值器),Curves(動畫曲線);

1、Animation

在 Flutter 中動畫本身和UI渲染沒有任何關係,Animation是一個抽象類,它擁有其當前值和狀態(完成或停止),Flutter 中的動畫系統就是基於 Animation 物件的。其中比較常用的就是Animation類是Animation。它可以通過其 value 屬性來獲取當前動畫的值。

Animation 除了可以生成 double 的值之外還可以生成如:顏色--Animation<Color> 或者大小--Animation<Size>

Animation 物件可以擁有 Listeners 和 StatusListeners 監聽器,可以用 addListener()addStatusListener() 來新增。只要動畫的的值發生變化,就會呼叫監聽器。正常我們在 Listeners 中呼叫setState() 來觸發UI重建;動畫開始、結束、向前移動或向後移動時會呼叫StatusListener。

2、AnimationController

AnimationController 是一個特殊的 Animation 物件,在螢幕重新整理的每一幀,就會生成一個新的值。預設情況下,AnimationController 會在特定的時間內線性的生成0.0到1.0的數字。AnimationController派生於 Animation<double>,因此可以在需要Animation物件的任何地方使用。不但如此,AnimationController還具有控制動畫的其他方法,比如 forward()方法可以啟動動畫。

AnimationController({
    double value,
    this.duration,
    this.reverseDuration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  })
複製程式碼

建立 AnimationController 必須需傳入 vsync,傳入 vsunc 是為了防止動畫的UI不在當前螢幕時,不需要繪製,從而防止消耗不必要的資源。通過將 SingleTickerProviderStateMixin 混入到類定義中,就可以將 statefu l物件作為 vsync 的值。

除了 vsync 還可以傳入正向動畫執行的時間 duration 以及反向動畫執行時間 reverseDuration 等。

常用函式:

序號 方法 介紹
1 forward() 開始播放動畫
2 stop() 停止動畫
3 reset() 重製動畫
4 reverse() 反向播放動畫,必須處於正向動畫播放完成的狀態之後才有用
5 dispose() 釋放動畫佔用資源
6 repeat() 迴圈播放動畫

注意:動畫完成時釋放控制器(呼叫 dispose() 方法)以防止記憶體洩漏

@override
void dispose() {
  animationController.dispose();
  super.dispose();
}
複製程式碼

3、Tween

預設情況下,AnimationController物件的範圍從0.0到1.0。如果您需要不同的範圍或不同的資料型別,則可以使用Tween來配置動畫以生成不同的範圍或資料型別的值。比如,可以生產從0-100的數字:

final Tween doubleTween = new Tween<double>(begin: 0.0, end: 100.0);
複製程式碼

Tween是一個無狀態(stateless)物件,繼承自Animatable<T>,而不是繼承自 Animation<T>。Tween 需要兩個值,分別是:begin 和 end。Tween的唯一職責就是定義從輸入範圍到輸出範圍的對映。

Animatable與Animation相似,不是必須輸出double值,也可以是顏色,比如,從白色到黑色:

final Tween colorTween = new ColorTween(begin: Colors.withe, end: Colors.black);
複製程式碼

Tween 可以通過 animate() 方法傳入 controller 物件建立 Animation 物件。如下

AnimationController _animationController = AnimationController(animationBehavior:AnimationBehavior.normal,vsync: this);
Tween<double> _tween = Tween<double>(begin: 0.0, end: 100.0)..animate(_animationController);
複製程式碼

4、CurvedAnimation

Curves 用來調整動畫過程中隨時間的變化率,預設情況下,動畫以均勻的線性模型變化。Flutter 內部也提供了一系列實現相應變化率的 Curves 物件:linea ------ 線性,decelerate ------ 減速等。

當然,也可以自定義繼承 Curves 的類來定義動畫的變化率,如:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}
複製程式碼

5、新增監聽

目前為止動畫只是實現了自身數值的變化,並沒有讓 Widget 動起來,這裡我們需要對動畫數值進行監聽,然後使用 setstatus 來更新 Widget 的屬性,從而使 Widget 動起來。

新增數值監聽:

Animation animation = CurvedAnimation(parent: _animationController, curve: Curves.linear);
    animation.addListener((){
      setState(() {
        
      });
    });
複製程式碼

除此之外我們還可以監聽動畫的狀態變更,當動畫結束時我們反轉動畫,當動畫的反轉也結束後我們從新開始動畫,這樣動畫就會一直這樣迴圈下去。

狀態變更監聽:

animation.addStatusListener((status){
      print(status);
    });
複製程式碼

6、AnimatedWidget

AnimatedWidget 類允許您從 setState() 呼叫中的動畫程式碼中分離出 widget 程式碼。AnimatedWidget 不需要維護一個 State 物件來儲存動畫。

以下程式碼為官方文件自定義 AnimatedLogo

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

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

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Container(
        margin: new EdgeInsets.symmetric(vertical: 10.0),
        height: animation.value,
        width: animation.value,
        child: new FlutterLogo(),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}
複製程式碼

AnimatedWidget 為什麼不需要維護一個 State 物件來儲存動畫呢?

AnimatedWidget 原始碼中看一看出 AnimatedWidget 是繼承自 StatefulWidget 類,在 AnimatedWidget 中,建立 state 是建立了 _AnimatedState,接著看 _AnimatedState 類部分原始碼:

abstract class AnimatedWidget extends StatefulWidget{
  
   @override
  _AnimatedState createState() => _AnimatedState();
  
}
複製程式碼

_AnimatedState 類的 initState 方法新增了監聽 _handleChange,並在 didUpdateWidgetdispose 方法中移除了,_handleChange 裡面只有一行程式碼就是 setState 方法:

_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);
}
複製程式碼

7、並行動畫

所謂的並行動畫就是一起執行多個動畫,在 Flutter 中可以在同一個動畫控制器上使用多個Tween,然後每個Tween管理動畫中的不同效果,從而實現多個動畫同時執行。

final AnimationController controller =
    new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
final Animation<double> sizeAnimation =
    new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation<double> opacityAnimation =
    new Tween(begin: 0.1, end: 1.0).animate(controller);
複製程式碼

可以通過sizeAnimation.value來獲取大小,通過opacityAnimation.value來獲取不透明度,但AnimatedWidget的建構函式只接受一個動畫物件。 為了解決這個問題,可以建立了自己的Tween物件並顯式計算了這些值。

build方法.evaluate()在父級的動畫物件上呼叫Tween函式以計算所需的sizeopacity值。

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

class AnimatedLogo extends AnimatedWidget {
  // The Tweens are static because they don't change.
  static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
  static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);

  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: new Container(
          margin: new EdgeInsets.symmetric(vertical: 10.0),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: new FlutterLogo(),
        ),
      ),
    );
  }
}
複製程式碼

例項

效果圖

Flutter 掌握動畫開發

1、縮放動畫

直接貼程式碼

///放大縮小動畫
  Widget scale() {
    return Column(
      children: <Widget>[
        Container(
          height: 170,
          child: Center(
            child: Container(
              width: _scaleAnimation.value,
              height: _scaleAnimation.value,
              child: new FlutterLogo(),
            ),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              color: Colors.blue,
              child: Text(
                "放大",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _scaleController.forward();
              },
            ),
            RaisedButton(
              color: Colors.red,
              child: Text(
                "縮小",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _scaleController.reverse();
              },
            )
          ],
        ),
      ],
    );
  }
複製程式碼

2、淡入淡出動畫

程式碼:

/// 淡入淡出
  Widget alpha() {
    return Column(
      children: <Widget>[
        Container(
          height: 170,
          child: Center(
            child: Container(
              height: 100,
              width: 100,
              child: Opacity(
                opacity: _alphaAnimation.value,
                child: FlutterLogo(),
              ),
            ),
          ),
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              color: Colors.blue,
              child: Text(
                "淡入",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _alphaController.forward();
              },
            ),
            RaisedButton(
              color: Colors.red,
              child: Text(
                "淡出",
                style: TextStyle(color: Colors.white),
              ),
              onPressed: () {
                _alphaController.reverse();
              },
            )
          ],
        ),
      ],
    );
  }
複製程式碼

注意,一個 Widget 使用多個animationController 需要修改混入SingleTickerProviderStateMixin 為 TickerProviderStateMixin。

結尾

完整程式碼奉上GitHub地址:fluter_demo ,歡迎star和fork。

到此,本文就結束了,如有不當之處敬請指正,一起學習探討,謝謝?。

Flutter 掌握動畫開發

相關文章