天天看點

flutter中的動畫基礎知識

1.動畫介紹

Flutter中的動畫系統基于

Animation

對象的,它不是一個Widget,這是因為

Animation

對象本身和UI渲染沒有任何關系。Animation是一個抽象類,就相當于一個定時器,它用于儲存動畫的插值和狀态,并執行數值的變化。widget可以在

build

函數中讀取

Animation

對象的目前值, 并且可以監聽動畫的狀态改變。

2.在Flutter中有哪些類型的動畫?

在Flutter中動畫分為兩類:基于tween或基于實體的。

  • 補間(Tween)動畫:在補間動畫中,定義了開始點和結束點、時間線以及定義轉換時間和速度的曲線。然後由架構計算如何從開始點過渡到結束點;
  • 基于實體的動畫:在基于實體的動畫中,運動被模拟為與真實世界的行為相似。例如,當你擲球時,它在何處落地,取決于抛球速度有多快、球有多重、距離地面有多遠。 類似地,将連接配接在彈簧上的球落下(并彈起)與連接配接到繩子上的球放下的方式也是不同;

3.Flutter中常用的動畫的API

在Flutter中使用動畫有幾個常用的API:

  • Animation:是Flutter動畫庫中的一個核心類,它生成指導動畫的值;
  • AnimationController:Animation的一個子類,用來管理Animation;
  • Tween:在正在執行動畫的對象所使用的資料範圍之間生成值。例如,Tween可生成從紅到藍之間的色值,或者從0到255;
  • AnimatedBuilder:是拆分動畫的一個工具類,借助它我們可以将動畫和widget進行分離,而AnimatedWidget了解為Animation的助手,使用它可以簡化我們對動畫的使用;
    • AnimatedBuilder與AnimatedWidget的最大差別是在于對動畫的拆分上AnimatedBuilder的模式可以将動畫邏輯和widget展示進行拆分,而AnimatedWidget是融合在一起的。
  • Curve:Flutter中可以通過Curve(曲線)來描述動畫過程;
  • Hero動畫:Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,是以在路由切換時會逐漸過渡,這樣就會産生一個Hero動畫
  • 組合動畫:有些時候我們可能會需要執行一個動畫序列執行一些複雜的動畫
  • Ticker:的作用是添加螢幕重新整理回調,每次螢幕重新整理都會調用

    TickerCallback;

Animation

在Flutter中,Animation對象本身和UI渲染沒有任何關系。Animation是一個抽象類,它擁有其目前值和狀态(完成或停止)。其中一個比較常用的Animation類是

Animation<double>

Flutter中的Animation對象是一個在一段時間内依次生成一個區間之間值的類。Animation對象的輸出可以是線性的、曲線的、一個步進函數或者任何其他可以設計的映射。 根據Animation對象的控制方式,動畫可以反向運作,甚至可以在中間切換方向。

  • Animation還可以生成除double之外的其他類型值,如:

    Animation<Color>

     或 

    Animation<Size>

  • Animation對象有狀态。可以通過通路其value屬性擷取動畫的目前值;
  • Animation對象本身和UI渲染沒有任何關系;

AnimationController

AnimationController

是一個特殊的

Animation

對象,在螢幕重新整理的每一幀,就會生成一個新的值。預設情況下,

AnimationController

在給定的時間段内會線性的生成從0.0到1.0的數字。 例如,下面代碼建立一個Animation對象:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);
           

AnimationController派生自

Animation<double>

,是以可以在需要Animation對象的任何地方使用。 但是,

AnimationController

具有控制動畫的其他方法:

  • forward()

    :啟動動畫;
  • reverse({double from})

    :倒放動畫;
  • reset()

    :重置動畫,将其設定到動畫的開始位置;
  • stop({ bool canceled = true })

    :停止動畫;

當建立一個AnimationController時,需要傳遞一個vsync參數,存在vsync時會防止螢幕外動畫消耗不必要的資源,可以将stateful對象作為vsync的值。

注意: 在某些情況下,值(position,值動畫的目前值)可能會超出AnimationController的0.0-1.0的範圍。例如,fling()函數允許您提供速度(velocity)、力量(force)、position(通過Force對象)。位置(position)可以是任何東西,是以可以在0.0到1.0範圍之外。 CurvedAnimation生成的值也可以超出0.0到1.0的範圍。根據選擇的曲線,CurvedAnimation的輸出可以具有比輸入更大的範圍。例如,Curves.elasticIn等彈性曲線會生成大于或小于預設範圍的值。

Tween

預設情況下,

AnimationController

對象值為:double類型,範圍是0.0到1.0 。如果我們需要不同的範圍或不同的資料類型,則可以使用Tween來配置動畫以生成不同的範圍或資料類型的值。例如,以下示例,Tween生成從-200.0到0.0的值:

final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
           

Tween是一個無狀态(stateless)對象,需要begin和end值。Tween的唯一職責就是定義從輸入範圍到輸出範圍的映射。輸入範圍通常為0.0到1.0,但這不是必須的。

Tween繼承自

Animatable<T>

,而不是繼承自

Animation<T>

。Animatable與Animation相似,不是必須輸出double值。例如,ColorTween指定兩種顔色之間的過渡。

final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);
           

Tween對象不存儲任何狀态。相反,它提供了

evaluate(Animation<double> animation)

方法将映射函數應用于動畫目前值。 Animation對象的目前值可以通過

value()

方法取到。evaluate函數還執行一些其它處理,例如分别確定在動畫值為0.0和1.0時傳回開始和結束狀态。

Tween.animate

要使用Tween對象,可調用它的

animate()

方法,傳入一個控制器對象。例如,以下代碼在500毫秒内生成從0到255的整數值。

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

注意

animate()

傳回的是一個Animation,而不是一個Animatable。

Curve

動畫過程預設是線性的(勻速),如果需要非線形的,比如:加速的或者先加速後減速等。Flutter中可以通過Curve(曲線)來描述動畫過程。以下示例建構了一個控制器、一條曲線和一個Tween:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
           
Curves曲線 動畫過程
linear 勻速的
decelerate 勻減速
ease 開始加速,後面減速
easeIn 開始慢,後面快
easeOut 開始快,後面慢
easeInOut 開始慢,然後加速,最後再減速

Hero動畫

Hero動畫就是在路由切換時,有一個共享的Widget可以在新舊路由間切換,由于共享的Widget在新舊路由頁面上的位置、外觀可能有所差異,是以在路由切換時會逐漸過渡,這樣就會産生一個Hero動畫。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
          appBar: AppBar(
            title: Text("首頁"),
          ),
          body: Route1()),
    );
  }
}

// 路由A
class Route1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.topCenter,
      child: InkWell(
        child: Hero(
          tag: "avatar", //唯一标記,前後兩個路由頁Hero的tag必須相同
          child: CircleAvatar(
            backgroundImage: AssetImage(
              "assets/banner.jpeg",
            ),
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return Route2();
          }));
        },
      ),
    );
  }
}

class Route2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Hero(
          tag: "avatar", //唯一标記,前後兩個路由頁Hero的tag必須相同
          child: Image.asset("assets/banner.jpeg")),
    );
  }
}

           

組合動畫

有些時候我們可能會需要執行一個動畫序列執行一些複雜的動畫。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: Route(),
    );
  }
}

class Route extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return RouteState();
  }
}

class RouteState extends State<Route> with SingleTickerProviderStateMixin {
  Animation<Color> color;
  Animation<double> width;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      // 動畫的時長
      duration: Duration(milliseconds: 2000),
      // 提供 vsync 最簡單的方式,就是直接繼承 SingleTickerProviderStateMixin
      vsync: this,
    );

    //高度動畫
    width = Tween<double>(
      begin: 100.0,
      end: 300.0,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          //間隔,前60%的動畫時間 1200ms執行高度變化
          0.0, 0.6,
        ),
      ),
    );

    color = ColorTween(
      begin: Colors.green,
      end: Colors.red,
    ).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(
          0.6, 1.0, //高度變化完成後 800ms 執行顔色編碼
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("首頁"),
      ),
      body: InkWell(
        ///1、不用顯式的去添加幀監聽器,再調用setState()
        ///2、縮小動畫建構的範圍,如果沒有builder,setState()将會在父widget上下文調用,導緻父widget的build方法重新調用,現在隻會導緻動畫widget的build重新調用
        child: AnimatedBuilder(
            animation: controller,
            builder: (context, child) {
              return Container(
                color: color.value,
                width: width.value,
                height: 100.0,
              );
            }),
        onTap: () {
          controller.forward().whenCompleteOrCancel(() => controller.reverse());
        },
      ),
    );
  }
}
           

Ticker

Ticker的作用是添加螢幕重新整理回調,每次螢幕重新整理都會調用

TickerCallback

。使用Ticker來驅動動畫會防止螢幕外動畫(動畫的UI不在目前螢幕時,如鎖屏時)消耗不必要的資源。因為Flutter中螢幕重新整理時會通知Ticker,鎖屏後螢幕會停止重新整理,是以Ticker就不會再觸發。最簡單的做法為将

SingleTickerProviderStateMixin

添加到State的定義中。

4.實作一個從小放大的動畫

在下面的執行個體中我們為一個logo添加了一個從小放大的動畫:

flutter中的動畫基礎知識
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

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

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

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // #docregion addListener
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        // #enddocregion addListener
        setState(() {
          animationValue = animation.value;
        });
        // #docregion addListener
      })
      ..addStatusListener((AnimationStatus state) {
        setState(() {
          animationState = state;
        });
      });
    // #enddocregion addListener
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 50),
      child: Column(
        children: <Widget>[
          GestureDetector(
            onTap: () {
              controller.reset();
              controller.forward();
            },
            child: Text('Start', textDirection: TextDirection.ltr),
          ),
          Text('State:' + animationState.toString(),
              textDirection: TextDirection.ltr),
          Text('Value:' + animationValue.toString(),
              textDirection: TextDirection.ltr),
          Container(
            height: animation.value,
            width: animation.value,
            child: FlutterLogo(),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}
           
注意,在上述代碼中要實作這個動畫的關鍵一步是在

addListener()

的回調中添加

setState

的調用這樣才能觸發頁面重新渲染,動畫才能有效,另外也可以通過AnimatedWidget來實作,在下文中會講到。

5.為動畫添加監聽器

有時我們需要知道動畫執行的進度和狀态,在Flutter中我們可以通過Animation的

addListener

addStatusListener

方法為動畫添加監聽器:

  • addListener

    :動畫的值發生變化時被調用;
  • addStatusListener

    :動畫狀态發生變化時被調用;
枚舉值 含義

dismissed

動畫在起始點停止

forward

動畫正在正向執行

reverse

動畫正在反向執行

completed

動畫在終點停止
@override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      // #enddocregion print-state
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })
      // #docregion print-state
      ..addStatusListener((state) => print('$state'));
      ..addListener(() {
        // #enddocregion addListener
        setState(() {
          // The state that has changed here is the animation object’s value.
        });
        // #docregion addListener
      });
    controller.forward();
  }
           

6.用AnimatedWidget與AnimatedBuilder簡化代碼

什麼是AnimatedWidget?

我們可以将

AnimatedWidget

了解為Animation的助手,使用它可以簡化我們對動畫的使用,在實作一個從小放大的動畫的學習中我們不難發現,在不使用

AnimatedWidget

的情況下需要手動調用動畫的

addListener()

并在回調中添加

setState

才能看到動畫效果,

AnimatedWidget

将為我們簡化這一操作。

在下面的重構示例中,LogoApp現在繼承自

AnimatedWidget

而不是

StatefulWidget

AnimatedWidget

在繪制時使用動畫的目前值。LogoApp仍然管理着

AnimationController

Tween

// Demonstrate a simple animation with AnimatedWidget

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

void main() {
  runApp(new LogoApp());
}
           

什麼是AnimatedBuilder?

AnimatedBuilder

是用于建構動畫的通用widget,AnimatedBuilder對于希望将動畫作為更大建構函數的一部分包含在内的更複雜的widget時非常有用,其實你可以這樣了解:AnimatedBuilder是拆分動畫的一個工具類,借助它我們可以将動畫和widget進行分離:

在上面的執行個體中我們的代碼存在的一個問題: 更改動畫需要更改顯示logo的widget。更好的解決方案是将職責分離:

  • 顯示logo
  • 定義Animation對象
  • 渲染過渡效果

接下來我們就借助

AnimatedBuilder

類來完成此分離。

AnimatedBuilder

是渲染樹中的一個獨立的類, 與

AnimatedWidget

類似,

AnimatedBuilder

自動監聽來自Animation對象的通知,不需要手動調用

addListener()

我們根據下圖的 widget 樹來建立我們的代碼:

flutter中的動畫基礎知識
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        child: FlutterLogo(),
      );
}
// #enddocregion LogoWidget

// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}
// #enddocregion GrowTransition

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

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }
  // #enddocregion print-state

  @override
  Widget build(BuildContext context) => GrowTransition(
        child: LogoWidget(),
        animation: animation,
      );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  // #docregion print-state
}
           

參考:https://mp.weixin.qq.com/s/IGHmnOS7E3oZjfq9PDpKzA