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

2021-03-08 18:01 更新

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

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

#基礎(chǔ)版本

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

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


//需要繼承TickerProvider,如果有多個(gè)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(()=>{});
      });
    //啟動(dòng)動(dòng)畫(huà)(正向執(zhí)行)
    controller.forward();
  }


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


  dispose() {
    //路由銷(xiāo)毀時(shí)需要釋放動(dòng)畫(huà)資源
    controller.dispose();
    super.dispose();
  }
}

上面代碼中addListener()函數(shù)調(diào)用了setState(),所以每次動(dòng)畫(huà)生成一個(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)畫(huà)完成時(shí)要釋放控制器(調(diào)用dispose()方法)以防止內(nèi)存泄漏。

上面的例子中并沒(méi)有指定 Curve,所以放大的過(guò)程是線性的(勻速),下面我們指定一個(gè) Curve,來(lái)實(shí)現(xiàn)一個(gè)類(lèi)似于彈簧效果的動(dòng)畫(huà)過(guò)程,我們只需要將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(() {
        });
      });
    //啟動(dòng)動(dòng)畫(huà)
    controller.forward();
  }

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

圖9-1圖9-2

#使用AnimatedWidget簡(jiǎn)化

細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)上面示例中通過(guò)addListener()setState() 來(lái)更新 UI 這一步其實(shí)是通用的,如果每個(gè)動(dòng)畫(huà)中都加這么一句是比較繁瑣的。AnimatedWidget類(lèi)封裝了調(diào)用setState()的細(xì)節(jié),并允許我們將 widget 分離出來(lái),重構(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);
    //啟動(dòng)動(dòng)畫(huà)
    controller.forward();
  }


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


  dispose() {
    //路由銷(xiāo)毀時(shí)需要釋放動(dòng)畫(huà)資源
    controller.dispose();
    super.dispose();
  }
}

#用AnimatedBuilder重構(gòu)

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

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

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

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

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

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

   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中正是通過(guò)這種方式封裝了很多動(dòng)畫(huà),如:FadeTransition、ScaleTransition、SizeTransition等,很多時(shí)候都可以復(fù)用這些預(yù)置的過(guò)渡類(lèi)。

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

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

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

#示例

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

  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) {
        //動(dòng)畫(huà)執(zhí)行結(jié)束時(shí)反向執(zhí)行動(dòng)畫(huà)
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //動(dòng)畫(huà)恢復(fù)到初始狀態(tài)時(shí)執(zhí)行動(dòng)畫(huà)(正向)
        controller.forward();
      }
    });


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)