Flutter 中的動畫

#白雲蒼狗發表於2021-08-12

Flutter 中動畫的建立有很多種, 需要根據具體的需求選擇不同的動畫。如果只是簡單的佈局等的動畫直接使用最簡單的隱式動畫就可以了,因為隱式動畫是由框架控制的,所以僅僅只需要更改變需要變化屬性就可以了。如果你想自己控制動畫的變換則需要使用顯示動畫,如果需要控制一些列動畫組合時使用交織動畫去控制。如果內建的滿足不了需求的時候,還可以結合畫布自繪動畫。

動畫基礎

Flutter動畫和其他平臺動畫原理也是一樣的,都是在快速更改UI實現動畫效果。在一個Flutter動畫中主要包含Animation(動畫)、AnimationController(控制器)、Curve(速度曲線)、Animatable(動畫取值範圍)、Listeners (監聽事件)、Ticker(幀)。

  • Animation 一個抽象類是Flutter動畫的核心類,主用於儲存動畫當前插值的和狀態,在動畫執行時會持續生成介於兩個值之間的插入值。例如當寬從100變成200,會在動畫第一幀到最後一幀都會生成100-200區間的一個值,如果速度是勻速的,這個值就是勻速增加到200。
  • AnimationController 用來控制動畫的狀態啟動、暫停、反向執行等, 是Animation的一個子類
  • Curve 用來定義動畫運動的是勻速運動還是勻加速等,和 css 中 animation-timing-function 類似
  • Animatable 用於表明動畫值範圍值。可以通過呼叫animate方法,返回一個Animation,常見的Tween系列的類都是對他的實現
  • Listener 監聽動畫狀態的變化
  • Ticker 幀回撥,在動畫執行時候每一幀都會呼叫其回撥,類似與 js 中的 requestAnimationFrame

動畫組成結構

結構圖

動畫選擇

流程圖

隱式動畫

隱式動畫簡單來說就是我們只需要修改對應的屬性,Flutter就是自己幫我們過渡動畫,和css中過渡有點類似,當我們設定後transition後只需要更改對應的css屬性就會自動過渡到新的值。Flutter 內建了一些常用的隱式動畫,可以看到原始碼裡都是對ImplicitlyAnimatedWidget的實現,如果需要我們也可以自己實現ImplicitlyAnimatedWidget來自定義隱式動畫。

內建隱式動畫

看個使用例子

// 首先我們在一個StatefulWidget定義 一個height和color
double heihgt = 100;
Color color = Colors.yellow[800];

// 在build 怎加一個隱式動畫組建 AnimatedContainer,需要個Duration(動畫執行時間),其他的引數和Container的基本一致
AnimatedContainer(
    duration: Duration(milliseconds: 500), 
    height: heihgt, // 使用我們定義好的值
    color: color,
    margin: EdgeInsets.all(8),
    child: Center(
        child: Text('AnimatedContainer',
            style: TextStyle(color: Colors.white, fontSize: 20),
        ),
    ),
)

// 在需要執行動畫時候我們修改 height 和 color 值,就會看到 上邊的組建會一邊變高邊過渡到藍色上
setState(() {
    heihgt = 200;
    color = Colors.blue;
});

在Flutter內建的隱式動畫元件中,一般都是AnimatedXxxxxx類似的,後面的Xxxxxx都能找到對應的元件。內建的有下面這些 AnimatedContainer、AnimatedPadding、AnimatedAlign、AnimatedPositioned、AnimatedOpacity、SliverAnimatedOpacity、AnimatedDefaultTextStyle、AnimatedPhysicalModel。這些隱式動畫的使用和其Xxxxxx對應的屬性基本一致,只需要額外的指定 duration 就可以了,當然也可以為動畫指定動畫曲線 curve。

自定義隱式動畫

當這內建的滿足不了你的時候,你也可以去實現一個隱式動畫,只需要實現抽象類 ImplicitlyAnimatedWidget。實現自定義隱式動畫僅需要重寫build 和 forEachTween 就可以簡單實現了。

// 直接繼承ImplicitlyAnimatedWidget 
class AnimatedDemo extends ImplicitlyAnimatedWidget {
  final Color color;
  final Widget child;
  final double height;

  AnimatedDemo({
    this.color,
    this.height,
    Curve curve = Curves.linear,
    this.child,
    @required Duration duration,
  }) : super(curve: curve, duration: duration);

  
  @override
  _AnimatedDemo createState() => _AnimatedDemo();
}
//因為ImplicitlyAnimatedWidget是繼承 StatefulWidget 的,所以還需要繼承他的狀態類 (AnimatedWidgetBaseState 繼承自 ImplicitlyAnimatedWidgetState)
class _AnimatedDemo extends AnimatedWidgetBaseState<AnimatedDemo> {
  ColorTween _color;
  Tween<double> _height;
 
  // 在動畫執行時候會每一幀都呼叫 build
  @override
  Widget build(BuildContext context) {
    return Container(
      color: _color.evaluate(animation), //使用evaluate可以獲取Tween當前幀的狀態值
      height: _height.evaluate(animation),
      child: widget.child,
    );
  }

  //首次build和更新時候會呼叫,在這裡設定動畫需要的Tween的開始值和結束值
  @override
  void forEachTween(visitor) {
    //visitor 有三個引數(當前的tween,動畫終止狀態,一個回撥函式(將第一次給定的值設定為Tween的開始值))
    _color = visitor(_color, widget.color, (value) => ColorTween(begin: value));// 這裡value==首次widget.color的值
    _height = visitor(_height, widget.height, (value) => Tween<double>(begin: value));
  }
}

我們可以去看 ImplicitlyAnimatedWidget 是如何控制動畫的,在 ImplicitlyAnimatedWidgetState 中會看到其實裡面定義了 AnimationController 控制動畫。然後可以看到 didUpdateWidget 鉤子函式中呼叫了 _controller.forward() 執行動畫,當父 Widget 呼叫 setState 時候就會觸發這個鉤子函式的呼叫。

顯示動畫

有時候有些動畫需要們自己去控制動畫的狀態,而不是交給框架去處理,這時就需要我們自己去定義前面簡介裡提到的那幾個動畫要素了。

內建顯示動畫

在Flutter中內建的顯示動畫大部分都是XxxxxxTransition名稱的,我們看個內建顯示動畫使用例子,RotationTransition元件需要一個 turns(Animation<double>)引數,我們可以給它個AnimationController

// RotationTransition 引數
RotationTransition(
   turns: Animation<double>,
   child: ChildWidget(),
)

// AnimationController 引數
AnimationController(
  double? value, // 初始值
  this.duration, //動畫時間
  this.reverseDuration, // 反向動畫執行的時間
  this.debugLabel, 
  this.lowerBound = 0.0, //動畫開始值
  this.upperBound = 1.0, //動畫結束值
  this.animationBehavior = AnimationBehavior.normal,
  required TickerProvider vsync, //垂直同步,需要一個 Ticker ,Flutter 給我們提供了
)

使用 RotationTransition,可以看到一個紅藍漸變色方塊旋轉一週。

class RotationTransitionDemo extends StatefulWidget {
  @override
  _RotationTransitionDemoState createState() => _RotationTransitionDemoState();
}

class _RotationTransitionDemoState extends State<RotationTransitionDemo> with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 設定動畫時間為1秒
    _controller = AnimationController(duration: Duration(milliseconds: 1000), vsync: this)
    ..addListener(() { // 監聽動畫的狀態值發生變化
        print(_controller.value);
    })
    ..addStatusListener((status) { //監聽動畫狀態
        // dismissed 動畫在起始點停止
        // forward 動畫正在正向執行
        // reverse 動畫正在反向執行
        // completed 動畫在終點停止
        print(status);
    })
    ..forward(); // 執行動畫
    // 常用方法
    // forward() // 正向執行動畫
    // reverse() 反向執行動畫
    // repeat() 重複執行 可以傳個引數 是否會反向運動
    // stop() 停止動畫
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('RotationTransition'),
      ),
      body: Center(
        child: RotationTransition(
          turns: _controller, // 設定 Animation
          child: Container(
            height: 300,
            width: 300,
            decoration: BoxDecoration(
              gradient: LinearGradient(colors: [Colors.red, Colors.blue]),
            ),
          ),
        ),
      ),
    );
  }
}

控制器補間和曲線

在控制器中我們可以看的動畫開始值和結束值預設是0.0到1.0,而且是double型別的。而實際動畫中不可能只是double型別的,需要我們自己使用Animatable來指定補間範圍值。
修改一下上面的程式碼

// 通過控制器的drive方法新增
 _controller = AnimationController(duration: Duration(milliseconds: 1000),vsync: this)
  ..drive(Tween(begin: 1, end: 4)) //使用Tween(Animatable的子類)指定補間範圍

// 我也也可以是使用Animatable的animate方法新增到控制器
Tween(begin: 1, end: 4).animate(_controller);
// 這樣寫我們可以使用 chain() 疊加多個 Tween
Tween(begin: 1, end: 4)
.chain(CurveTween(curve: Curves.ease)) //疊加個曲線
.animate(_controller);

Flutter已經內建幫我們實現了很多Animatable,ColorTween、SizeTween、IntTween、StepTween等等。

自定義顯示動畫

檢視 RotationTransition 的原始碼,我們可以看到它是對的抽象類 AnimatedWidget 的實現,當內建的滿足不了我們的時候,可以直接自己實現 AnimatedWidget 自定義顯示動畫。先來看看 AnimatedWidget 裡面都有些啥。

// 只摘取主要的部分
abstract class AnimatedWidget extends StatefulWidget {

  const AnimatedWidget({ Key key,@required this.listenable,  }) : assert(listenable != null), super(key: key);

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

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

  void _handleChange() {
    setState(() {
      // 我們可以看到顯示動畫是通過控制器監聽插值更改 setState 進行重繪。
    });
  }
}

接下來我自己繼承 AnimatedWidget 實現一個自定義顯示動畫

// 繼承 AnimatedWidget
class OpacityAnimatedWidget extends AnimatedWidget {
  final Widget child;
  Animation<Color> colorAnimation;

  // AnimatedWidget 需要可傳遞一個 listenable 進去,我們可以傳遞個 AnimationController
  OpacityAnimatedWidget(listenable, {this.colorAnimation, this.child}) : super(listenable: listenable);

  @override
  Widget build(BuildContext context) {
    Animation<double> animation = listenable;
    return Opacity(
      opacity: animation.value,
      child: Container(
        color: colorAnimation.value,
        child: child,
      ),
    );
  }
}

// 使用 需要在狀態類上 混入一個 SingleTickerProviderStateMixin
AnimationController _controller = AnimationController(duration: Duration(milliseconds: 1000), vsync: this); 

OpacityAnimatedWidget(
  Tween(begin: 1.0, end: .8).animate(_controller),
  colorAnimation: ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller),
  child: Container(
    height: 300,
    width: 300,
  ),
)

Flutter 內部還提供了一個 AnimatedBuilder 幫助我們簡化自定義動畫。

// 只需要三個三引數
AnimatedBuilder( 
  animation, // 一個listenable
  child,// 傳入個子元件,非必填
  builder,// (BuildContext context, Widget child){}  這裡的第二個引數 child ,就是上面傳入的 child
  // 這麼做的好處就是,動畫執行的時候只會執行 builder ,如果一個動畫只是包裹層需要執行動畫,這個時候就可以把包裹的子元件 放到外面傳進去
  // 這樣就每次只需要 執行 builder 而方法第二個引數是傳遞進來的引用,所以可以避免每次都更新,減少開銷
)

交織動畫

官方是這麼介紹的:交織動畫是一個簡單的概念:視覺變化是隨著一系列的動作發生,而不是一次性的動作。動畫可能是純粹順序的,一個改變隨著一個改變發生,動畫也可能是部分或者全部重疊的。動畫也可能有間隙,沒有變化發生。

簡單點說就是一個動畫可以分割成很多片段,每個片段都有不同的Tween,看個使用示例

class StaggeredAnimationDemo extends StatefulWidget {
  @override
  _StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _height;
  Animation<Color> _color;
  Animation<double> _borderRadius;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: Duration(milliseconds: 5000), vsync: this);

    _height = Tween(begin: 50.0, end: 300.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0, 0.15), // Interval 範圍必須是0-1 指定Tween在哪一段時間執行
      ),
    );

    _color = ColorTween(begin: Colors.red, end: Colors.blue).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.1, 0.2),
      ),
    );

    _borderRadius = Tween(begin: 10.0, end: 150.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.1, 0.25),
      ),
    );

    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BasiceAppLayout(
      title: '交織動畫',
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Container(
              height: _height.value,
              width: _height.value,
              decoration: BoxDecoration(
                color: _color.value,
                borderRadius: BorderRadius.circular(_borderRadius.value),
              ),
            );
          },
        ),
      ),
    );
  }
}

Hero動畫

Flutter叫它主動畫,用於不同頁面之間切換時候動畫,比如有一個商品列表,點選後跳到一個新的頁面檢視原圖,就可以這個動畫。使用也很簡單,在不同頁面使用Hero包裹需要動畫元件,兩個頁面的 tag 需要甚至成一直,但是同一個頁面需要保持唯一。

Hero(
  tag: "avatar", //唯一標記,前後兩個路由頁Hero的tag必須相同
  child: ChildWidget(),
)

相關文章