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

2021-03-08 18:01 更新

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

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

#基礎(chǔ)版本

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

class ScaleAnimationRoute extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}


//需要繼承TickerProvider,如果有多個AnimationController,則應(yīng)該使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>  with SingleTickerProviderStateMixin{ 

    
  Animation<double> animation;
  AnimationController controller;

    
  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        setState(()=>{});
      });
    //啟動動畫(正向執(zhí)行)
    controller.forward();
  }


  @override
  Widget build(BuildContext context) {
    return new Center(
       child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }


  dispose() {
    //路由銷毀時需要釋放動畫資源
    controller.dispose();
    super.dispose();
  }
}

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

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //使用彈性曲線
    animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(animation)
      ..addListener(() {
        setState(() {
        });
      });
    //啟動動畫
    controller.forward();
  }

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

圖9-1圖9-2

#使用AnimatedWidget簡化

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

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


  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: Image.asset("imgs/avatar.png",
          width: animation.value,
          height: animation.value
      ),
    );
  }
}




class ScaleAnimationRoute1 extends StatefulWidget {
  @override
  _ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}


class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
    with SingleTickerProviderStateMixin {


  Animation<double> animation;
  AnimationController controller;


  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 3), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    //啟動動畫
    controller.forward();
  }


  @override
  Widget build(BuildContext context) {
    return AnimatedImage(animation: animation,);
  }


  dispose() {
    //路由銷毀時需要釋放動畫資源
    controller.dispose();
    super.dispose();
  }
}

#用AnimatedBuilder重構(gòu)

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

@override
Widget build(BuildContext context) {
  //return AnimatedImage(animation: animation,);
    return AnimatedBuilder(
      animation: animation,
      child: Image.asset("images/avatar.png"),
      builder: (BuildContext ctx, Widget child) {
        return new Center(
          child: Container(
              height: animation.value, 
              width: animation.value, 
              child: child,
          ),
        );
      },
    );
}

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

也許你會說這和我們剛開始的示例差不了多少,其實它會帶來三個好處:

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

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

  1. 通過AnimatedBuilder可以封裝常見的過渡效果來復(fù)用動畫。下面我們通過封裝一個GrowTransition來說明,它可以對子 widget 實現(xiàn)放大動畫:

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

   
     final Widget child;
     final Animation<double> animation;

       
     Widget build(BuildContext context) {
       return new Center(
         child: new AnimatedBuilder(
             animation: animation,
             builder: (BuildContext context, Widget child) {
               return new Container(
                   height: animation.value, 
                   width: animation.value, 
                   child: child
               );
             },
             child: child
         ),
       );
     }
   }

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

   ...
   Widget build(BuildContext context) {
       return GrowTransition(
       child: Image.asset("images/avatar.png"), 
       animation: animation,
       );
   }

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

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

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

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

#示例

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(seconds: 1), vsync: this);
    //圖片寬高從0變到300
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //動畫執(zhí)行結(jié)束時反向執(zhí)行動畫
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //動畫恢復(fù)到初始狀態(tài)時執(zhí)行動畫(正向)
        controller.forward();
      }
    });


    //啟動動畫(正向)
    controller.forward();
  }
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號