Flutter實(shí)戰(zhàn) 動(dòng)畫基本結(jié)構(gòu)及狀態(tài)監(jiān)聽

2021-03-08 18:01 更新

#9.2.1 動(dòng)畫基本結(jié)構(gòu)

在 Flutter 中我們可以通過多種方式來(lái)實(shí)現(xiàn)動(dòng)畫,下面通過一個(gè)圖片逐漸放大示例的不同實(shí)現(xiàn)來(lái)演示 Flutter 中動(dòng)畫的不同實(shí)現(xiàn)方式的區(qū)別。

#基礎(chǔ)版本

下面我們演示一下最基礎(chǔ)的動(dòng)畫實(shí)現(xiàn)方式:

  1. class ScaleAnimationRoute extends StatefulWidget {
  2. @override
  3. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  4. }
  5. //需要繼承TickerProvider,如果有多個(gè)AnimationController,則應(yīng)該使用TickerProviderStateMixin。
  6. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin{
  7. Animation<double> animation;
  8. AnimationController controller;
  9. initState() {
  10. super.initState();
  11. controller = new AnimationController(
  12. duration: const Duration(seconds: 3), vsync: this);
  13. //圖片寬高從0變到300
  14. animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
  15. ..addListener(() {
  16. setState(()=>{});
  17. });
  18. //啟動(dòng)動(dòng)畫(正向執(zhí)行)
  19. controller.forward();
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. return new Center(
  24. child: Image.asset("imgs/avatar.png",
  25. width: animation.value,
  26. height: animation.value
  27. ),
  28. );
  29. }
  30. dispose() {
  31. //路由銷毀時(shí)需要釋放動(dòng)畫資源
  32. controller.dispose();
  33. super.dispose();
  34. }
  35. }

上面代碼中addListener()函數(shù)調(diào)用了setState(),所以每次動(dòng)畫生成一個(gè)新的數(shù)字時(shí),當(dāng)前幀被標(biāo)記為臟(dirty),這會(huì)導(dǎo)致 widget 的build()方法再次被調(diào)用,而在build()中,改變 Image 的寬高,因?yàn)樗母叨群蛯挾痊F(xiàn)在使用的是animation.value ,所以就會(huì)逐漸放大。值得注意的是動(dòng)畫完成時(shí)要釋放控制器(調(diào)用dispose()方法)以防止內(nèi)存泄漏。

上面的例子中并沒有指定 Curve,所以放大的過程是線性的(勻速),下面我們指定一個(gè) Curve,來(lái)實(shí)現(xiàn)一個(gè)類似于彈簧效果的動(dòng)畫過程,我們只需要將initState中的代碼改為下面這樣即可:

  1. initState() {
  2. super.initState();
  3. controller = new AnimationController(
  4. duration: const Duration(seconds: 3), vsync: this);
  5. //使用彈性曲線
  6. animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
  7. //圖片寬高從0變到300
  8. animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
  9. ..addListener(() {
  10. setState(() {
  11. });
  12. });
  13. //啟動(dòng)動(dòng)畫
  14. controller.forward();
  15. }

上面代碼執(zhí)行后截取了其中的兩幀,效果如圖9-1、9-2所示:

圖9-1圖9-2

#使用AnimatedWidget簡(jiǎn)化

細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)上面示例中通過addListener()setState() 來(lái)更新 UI 這一步其實(shí)是通用的,如果每個(gè)動(dòng)畫中都加這么一句是比較繁瑣的。AnimatedWidget類封裝了調(diào)用setState()的細(xì)節(jié),并允許我們將 widget 分離出來(lái),重構(gòu)后的代碼如下:

  1. class AnimatedImage extends AnimatedWidget {
  2. AnimatedImage({Key key, Animation<double> animation})
  3. : super(key: key, listenable: animation);
  4. Widget build(BuildContext context) {
  5. final Animation<double> animation = listenable;
  6. return new Center(
  7. child: Image.asset("imgs/avatar.png",
  8. width: animation.value,
  9. height: animation.value
  10. ),
  11. );
  12. }
  13. }
  14. class ScaleAnimationRoute1 extends StatefulWidget {
  15. @override
  16. _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
  17. }
  18. class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
  19. with SingleTickerProviderStateMixin {
  20. Animation<double> animation;
  21. AnimationController controller;
  22. initState() {
  23. super.initState();
  24. controller = new AnimationController(
  25. duration: const Duration(seconds: 3), vsync: this);
  26. //圖片寬高從0變到300
  27. animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
  28. //啟動(dòng)動(dòng)畫
  29. controller.forward();
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. return AnimatedImage(animation: animation,);
  34. }
  35. dispose() {
  36. //路由銷毀時(shí)需要釋放動(dòng)畫資源
  37. controller.dispose();
  38. super.dispose();
  39. }
  40. }

#用AnimatedBuilder重構(gòu)

用 AnimatedWidget 可以從動(dòng)畫中分離出 widget,而動(dòng)畫的渲染過程(即設(shè)置寬高)仍然在 AnimatedWidget 中,假設(shè)如果我們?cè)偬砑右粋€(gè) widget 透明度變化的動(dòng)畫,那么我們需要再實(shí)現(xiàn)一個(gè) AnimatedWidget,這樣不是很優(yōu)雅,如果我們能把渲染過程也抽象出來(lái),那就會(huì)好很多,而 AnimatedBuilder 正是將渲染邏輯分離出來(lái), 上面的 build 方法中的代碼可以改為:

  1. @override
  2. Widget build(BuildContext context) {
  3. //return AnimatedImage(animation: animation,);
  4. return AnimatedBuilder(
  5. animation: animation,
  6. child: Image.asset("images/avatar.png"),
  7. builder: (BuildContext ctx, Widget child) {
  8. return new Center(
  9. child: Container(
  10. height: animation.value,
  11. width: animation.value,
  12. child: child,
  13. ),
  14. );
  15. },
  16. );
  17. }

上面的代碼中有一個(gè)迷惑的問題是,child看起來(lái)像被指定了兩次。但實(shí)際發(fā)生的事情是:將外部引用child傳遞給AnimatedBuilderAnimatedBuilder再將其傳遞給匿名構(gòu)造器, 然后將該對(duì)象用作其子對(duì)象。最終的結(jié)果是AnimatedBuilder返回的對(duì)象插入到 widget 樹中。

也許你會(huì)說(shuō)這和我們剛開始的示例差不了多少,其實(shí)它會(huì)帶來(lái)三個(gè)好處:

  1. 不用顯式的去添加幀監(jiān)聽器,然后再調(diào)用setState() 了,這個(gè)好處和AnimatedWidget是一樣的。

  1. 動(dòng)畫構(gòu)建的范圍縮小了,如果沒有builder,setState()將會(huì)在父組件上下文中調(diào)用,這將會(huì)導(dǎo)致父組件的build方法重新調(diào)用;而有了builder之后,只會(huì)導(dǎo)致動(dòng)畫 widget 自身的build重新調(diào)用,避免不必要的 rebuild。

  1. 通過AnimatedBuilder可以封裝常見的過渡效果來(lái)復(fù)用動(dòng)畫。下面我們通過封裝一個(gè)GrowTransition來(lái)說(shuō)明,它可以對(duì)子 widget 實(shí)現(xiàn)放大動(dòng)畫:

  1. class GrowTransition extends StatelessWidget {
  2. GrowTransition({this.child, this.animation});
  3. final Widget child;
  4. final Animation<double> animation;
  5. Widget build(BuildContext context) {
  6. return new Center(
  7. child: new AnimatedBuilder(
  8. animation: animation,
  9. builder: (BuildContext context, Widget child) {
  10. return new Container(
  11. height: animation.value,
  12. width: animation.value,
  13. child: child
  14. );
  15. },
  16. child: child
  17. ),
  18. );
  19. }
  20. }

這樣,最初的示例就可以改為:

  1. ...
  2. Widget build(BuildContext context) {
  3. return GrowTransition(
  4. child: Image.asset("images/avatar.png"),
  5. animation: animation,
  6. );
  7. }

Flutter中正是通過這種方式封裝了很多動(dòng)畫,如:FadeTransition、ScaleTransition、SizeTransition等,很多時(shí)候都可以復(fù)用這些預(yù)置的過渡類。

#9.2.2 動(dòng)畫狀態(tài)監(jiān)聽

上面說(shuō)過,我們可以通過AnimationaddStatusListener()方法來(lái)添加動(dòng)畫狀態(tài)改變監(jiān)聽器。Flutter 中,有四種動(dòng)畫狀態(tài),在AnimationStatus枚舉類中定義,下面我們逐個(gè)說(shuō)明:

枚舉值 含義
dismissed 動(dòng)畫在起始點(diǎn)停止
forward 動(dòng)畫正在正向執(zhí)行
reverse 動(dòng)畫正在反向執(zhí)行
completed 動(dòng)畫在終點(diǎn)停止

#示例

我們將上面圖片放大的示例改為先放大再縮小再放大……這樣的循環(huán)動(dòng)畫。要實(shí)現(xiàn)這種效果,我們只需要監(jiān)聽動(dòng)畫狀態(tài)的改變即可,即:在動(dòng)畫正向執(zhí)行結(jié)束時(shí)反轉(zhuǎn)動(dòng)畫,在動(dòng)畫反向執(zhí)行結(jié)束時(shí)再正向執(zhí)行動(dòng)畫。代碼如下:

  1. initState() {
  2. super.initState();
  3. controller = new AnimationController(
  4. duration: const Duration(seconds: 1), vsync: this);
  5. //圖片寬高從0變到300
  6. animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
  7. animation.addStatusListener((status) {
  8. if (status == AnimationStatus.completed) {
  9. //動(dòng)畫執(zhí)行結(jié)束時(shí)反向執(zhí)行動(dòng)畫
  10. controller.reverse();
  11. } else if (status == AnimationStatus.dismissed) {
  12. //動(dòng)畫恢復(fù)到初始狀態(tài)時(shí)執(zhí)行動(dòng)畫(正向)
  13. controller.forward();
  14. }
  15. });
  16. //啟動(dòng)動(dòng)畫(正向)
  17. controller.forward();
  18. }
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)