Day14 - Flutter - 動畫

概述

  • 動畫API認識
  • 動畫案例練習
  • 其它動畫補充
一、動畫API認識

動畫實際上是我們通過某些方式(某種對象茬射,Animation對象)給Flutter引擎提供不同的值却音,而Flutter可以根據(jù)我們提供的值,給對應的小部件添加順滑的動畫效果焕檬。

  • 1.1宴霸、Animation
    在Flutter中囱晴,實現(xiàn)動畫的核心類是動畫,小部件可以直接將這些動畫合并到自己的構建方法中來讀取它們的當前值或監(jiān)聽其狀態(tài)變化瓢谢。
    我們一起來看一下Animation這個類畸写,它是一個抽象類

    • addListener方法(監(jiān)聽動畫值的概念)
      • 建立動畫的狀態(tài)值發(fā)生變化時,動畫都會通知所有通過addListener添加的監(jiān)聽器氓扛。
      • 通常枯芬,一個正在監(jiān)聽的動畫的state對象會調用自身的setState方法,將自身本身作為這些監(jiān)聽器的插入函數(shù)來通知小部件采郎,系統(tǒng)需要根據(jù)新狀態(tài)值進行重新生成千所。
    • addStatusListener(監(jiān)聽動畫狀態(tài)的改變)
      • 當動畫的狀態(tài)發(fā)生變化時,會通知所有通過addStatusListener添加的監(jiān)聽器蒜埋。

      • 通常情況下淫痰,動畫會從dismissed狀態(tài)開始,表示它處于變化區(qū)間的開始點整份。
        舉例來說待错,從0.0到1.0的動畫在dismissed狀態(tài)時的值應該是0.0。

      • 動畫進行的下一狀態(tài)可能是forward(例如從0.0到1.0)或者reverse(例如從1.0到0.0)烈评。

      • 最終火俄,如果動畫到達其區(qū)間的結束點(例如1.0),則動畫會變成completed狀態(tài)讲冠。

        abstractclass Animation<T> extends Listenable implements ValueListenable<T> {
            const Animation();
        
            // 添加動畫監(jiān)聽器
            @override
            void addListener(VoidCallback listener);
        
            // 移除動畫監(jiān)聽器
            @override
            void removeListener(VoidCallback listener);
        
            // 添加動畫狀態(tài)監(jiān)聽器
            void addStatusListener(AnimationStatusListener listener);
        
            // 移除動畫狀態(tài)監(jiān)聽器
            void removeStatusListener(AnimationStatusListener listener);
        
            // 獲取動畫當前狀態(tài)
            AnimationStatus get status;
        
            // 獲取動畫當前的值
            @override
            T get value;
        }
        
  • 1.2瓜客、AnimationController
    Animation是一個抽象類,并不能直接創(chuàng)建對象實現(xiàn)動畫的使用。
    AnimationController是Animation的一個子類谱仪,實現(xiàn)動畫通常我們需要創(chuàng)建AnimationController對象玻熙。

    • AnimationController會生成一系列的值,交替情況下值是0.0到1.0區(qū)間的值芽卿;

    除了上面的監(jiān)聽器揭芍,獲取動畫的狀態(tài)胳搞,值之外卸例,AnimationController還提供了對動畫的控制:

    • forward:向前執(zhí)行動畫
    • 反向:方向播放動畫
    • stop:停止動畫

    AnimationController的源碼:

    class AnimationController extends Animation<double>  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
       AnimationController({
           // 初始化值
           double value,
           // 動畫執(zhí)行的時間
           this.duration,
           // 反向動畫執(zhí)行的時間
           this.reverseDuration,
           // 最小值
           this.lowerBound = 0.0,
           // 最大值
           this.upperBound = 1.0,
           // 刷新率ticker的回調(看下面詳細解析)
           @required TickerProvider vsync,
       })
    }
    
    • AnimationController有一個必傳的參數(shù)vsync,它是什么呢肌毅?
      • Flutter的渲染閉環(huán)筷转,F(xiàn)lutter每次渲染一幀畫面之前都需要等待一個vsync信號。
      • 這里也是為了監(jiān)聽vsync信號悬而,當Flutter開發(fā)的應用程序不再接受同步信號時(比如鎖屏或退到后臺)呜舒,那么繼續(xù)執(zhí)行動畫會消耗性能。
      • 這個時候我們設置了Ticker笨奠,就不會再出發(fā)動畫了袭蝗。
      • 開發(fā)中比較常見的是將 SingleTickerProviderStateMixin 混入到State的定義中。
  • 1.3般婆、CurvedAnimation(設置動畫執(zhí)行的速率-速率曲線)
    CurvedAnimation也是Animation的一個實現(xiàn)類到腥,它的目的是為了給AnimationController增加動畫曲線:
    CurvedAnimation可以將AnimationControllerCurve結合起來,生成一個新的Animation對象

    class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
        CurvedAnimation({
            // 通常傳入一個AnimationController
            @requiredthis.parent,
            // Curve類型的對象
            @requiredthis.curve,
            this.reverseCurve,
        });
    }
    

    官方也發(fā)表了自己的定義Curse的一個示例

    import'dart:math';
    
    class ShakeCurve extends Curve {
        @override
        double transform(double t) => sin(t * pi * 2);
    }
    
  • 1.4啤咽、Tween
    默認情況下晋辆,AnimationController動畫生成的值所在區(qū)間是0.0到1.0
    如果希望使用這個以外的值,或者其他的數(shù)據(jù)類型宇整,就需要使用Tween
    Tween的源碼:源碼非常簡單瓶佳,預設兩個值即可,可以定義一個范圍鳞青。

    class Tween<T extends dynamic> extends Animatable<T> {
       // begin 開始值霸饲,end 結束值
       Tween({ this.begin, this.end });
    }
    

    Tween也有一些子類,比如ColorTween盼玄、BorderTween贴彼,可以針對動畫或者邊框來設置動畫的值。
    Tween.animate
    要使用Tween對象埃儿,需要調用Tween的animate()方法器仗,傳入一個Animation對象。

二、動畫案例練習
  • 2.1. 動畫的基本使用(不可取精钮,優(yōu)缺點)
    我們來完成一個案例:

    • 點擊案例后執(zhí)行一個心跳動畫威鹿,可以反復執(zhí)行

    • 再次點擊可以暫停和重新開始動畫


      import 'package:flutter/material.dart';
      
      void main() => runApp(MyApp());
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
           return MaterialApp(
               title: 'Flutter Demo',
               theme: ThemeData(
                   primarySwatch: Colors.blue, splashColor: Colors.transparent),
                   home: HYHomePage(),
               );
           }
      }
      
      class HYHomePage extends StatefulWidget {
         @override
          _HYHomePageState createState() => _HYHomePageState();
      }
      
      class _HYHomePageState extends State<HYHomePage>  with SingleTickerProviderStateMixin {
          // 創(chuàng)建AnimationController
          AnimationController _controller;
          Animation _animation;
          Animation _sizeAnim;
      
          @override
          void initState() {
              super.initState();
      
              // 1.創(chuàng)建AnimationController
              _controller = AnimationController(
                   vsync: this,
                   duration: Duration(seconds: 2)
              );
      
              // 2.動畫添加Curve效果
              _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
      
              // 3.Tween 設置值的范圍
              _sizeAnim = Tween(begin: 50.0, end: 150.0).animate(_animation);
      
              // 4.監(jiān)聽動畫值的改變
              _controller.addListener(() {
                 setState(() {});
              });
      
              // 5.監(jiān)聽動畫的狀態(tài)改變
              _controller.addStatusListener((status) {
                 if (status == AnimationStatus.completed) {
                      _controller.reverse();
                 } else if (status == AnimationStatus.dismissed) {
                      _controller.forward();
                 }
              });
          }
      
          @override
          Widget build(BuildContext context) {
             print("執(zhí)行_HYHomePageState的build方法");
             return Scaffold(
                appBar: AppBar(
                   title: Text("首頁"),
                ),
                body: return Center(
                   child: Icon(Icons.favorite, color: Colors.red, size: _sizeAnim.value,),
                );
                floatingActionButton: FloatingActionButton(
                  child: Icon(Icons.play_arrow),
                  onPressed: () {
                      if (_controller.isAnimating) {
                          _controller.stop();
                          print(_controller.status);
                      } else if (_controller.status == AnimationStatus.forward) {
                          _controller.forward();
                      } else if (_controller.status == AnimationStatus.reverse) {
                          _controller.reverse();
                      } else {
                          _controller.forward();
                      }
                  },
                ),
             );
          }
      
          @override
          void dispose() {
             _controller.dispose();
             super.dispose();
          }
      }
      
  • 2.2、AnimatedWidget(不可取轨香,優(yōu)缺點)
    在上面的代碼中忽你,我們必須監(jiān)聽動畫值的改變,并且改變后需要調用setState(也就是上面的第4步)臂容,這會帶來兩個問題:

    • 1.執(zhí)行動畫必須包含這部分代碼科雳,代碼比較冗余
    • 2.調用setState意味著整個State類中的build方法就會被重新build

    如何可以優(yōu)化上面的操作:創(chuàng)建一個Widget繼承自AnimatedWidget:

    class IconAnimation extends AnimatedWidget {
       IconAnimation(Animation animation): super(listenable: animation);
    
       @override
       Widget build(BuildContext context) {
           Animation animation = listenable;
           return Icon(Icons.favorite, color: Colors.red, size: animation.value,);
       }
    }
    

    那么2.1中的 的 第四步就可以去掉了,在Icon調用的地方直接:IconAnimation(_animation)

    • 缺點是:1脓杉、每次都需要創(chuàng)建一個類糟秘,類里面的build也會打印球散;2尿赚、如果創(chuàng)建的Widget有子類,那么子類依然會重復的build
  • 2.3蕉堰、AnimatedBuilder(優(yōu)解)
    AnimatedBuilder 可以解決上面 AnimatedWidget 產生的兩個問題凌净,代碼如下

    class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin {
       // 創(chuàng)建AnimationController
       AnimationController _controller;
       Animation _animation;
    
       @override
       void initState() {
           super.initState();
    
           // 1.創(chuàng)建AnimationController
           _controller = AnimationController(
               vsync: this,
               duration: Duration(seconds: 2)
           );
    
           // 2.設置Curve的值
           _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
    
           // 3.Tween
           _animation = Tween(begin: 50.0, end: 150.0).animate(_animation);
    
           // 監(jiān)聽動畫的狀態(tài)改變
           _controller.addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                    _controller.reverse();
              } else if (status == AnimationStatus.dismissed) {
                    _controller.forward();
              }
           });
        }
    
        @override
         Widget build(BuildContext context) {
            print("執(zhí)行_HYHomePageState的build方法");
            return Scaffold(
                appBar: AppBar(
                    title: Text("首頁"),
                ),
                body: Center(
                    child: AnimatedBuilder(
                       animation: _controller,
                       builder: (ctx, child) {
                           return Icon(Icons.favorite, color: Colors.red, size: _animation.value,);
                       },
                    ),
                ),
                floatingActionButton: FloatingActionButton(
                    child: Icon(Icons.play_arrow),
                    onPressed: () {
                        if (_controller.isAnimating) {
                            _controller.stop();
                            print(_controller.status);
                        } else if (_controller.status == AnimationStatus.forward) {
                            _controller.forward();
                        } else if (_controller.status == AnimationStatus.reverse) {
                            _controller.reverse();
                        } else {
                            _controller.forward();
                        }
                    },
                 ),
            );
         }
    
         @override
         void dispose() {
            _controller.dispose();
            super.dispose();
         }
    }
    
三、其它動畫補充
  • 3.1屋讶、交織動畫(多個動畫同時執(zhí)行)冰寻、
    案例說明:點擊floatingActionButton執(zhí)行動畫
    動畫集合了透明度變化大小變化丑婿、顏色變化性雄、旋轉動畫等;
    我們這里是通過多個Tween生成了多個Animation對象羹奉;
    代碼如下

    class HYHomePage extends StatefulWidget {
       @override
       _HYHomePageState createState() => _HYHomePageState();
    }
    
    class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin {
       // 創(chuàng)建AnimationController
       AnimationController _controller;
       Animation _animation;
    
       // 大小
       Animation<double> _sizeAnim;
       // 顏色
       Animation _colorAnim;
       // 透明度
       Animation<double> _opactiyAnim;
       // 角度
       Animation<double> _radiansAnim;
    
       @override
       void initState() {
         super.initState();
    
         // 1.創(chuàng)建AnimationController
         _controller = AnimationController(
            vsync: this,
            duration: Duration(seconds: 2)
         );
    
         // 2.設置Curve的值
         _animation = CurvedAnimation(parent: _controller, curve: Curves.linear);
    
         // 3.Tween
         _sizeAnim = Tween(begin: 10.0, end: 150.0).animate(_controller);
         _colorAnim = ColorTween(begin: Colors.brown, end: Colors.green).animate(_controller);
         _opactiyAnim = Tween(begin: 0.0, end: 1.0).animate(_controller);
         _radiansAnim = Tween(begin: 0.0, end: 2 * pi).animate(_controller);
    
         // 監(jiān)聽動畫的狀態(tài)改變
         _controller.addStatusListener((status) {
              if (status == AnimationStatus.completed) {
                 _controller.reverse();
              } else if (status == AnimationStatus.dismissed) {
                 _controller.forward();
              }
         });
        }
    
        @override
        Widget build(BuildContext context) {
           print("執(zhí)行_HYHomePageState的build方法");
           return Scaffold(
               appBar: AppBar(
                  title: Text("首頁"),
               ),
               body: Center(
                  child: AnimatedBuilder(
                     animation: _controller,
                     builder: (ctx, child) {
                        return Opacity(
                          opacity: _opactiyAnim.value,
                          child: Transform(
                              transform: Matrix4.rotationZ(_radiansAnim.value),
                              alignment: Alignment.center,
                              child: Container(
                                  width: _sizeAnim.value,
                                  height: _sizeAnim.value,
                                  color: _colorAnim.value,
                              ),
                          ),
                       );
                     },
                  )
               ),
               floatingActionButton: FloatingActionButton(
                  child: Icon(Icons.play_arrow),
                  onPressed: () {
                       if (_controller.isAnimating) {
                           _controller.stop();
                           print(_controller.status);
                       } else if (_controller.status == AnimationStatus.forward) {
                           _controller.forward();
                       } else if (_controller.status == AnimationStatus.reverse) {
                           _controller.reverse();
                       } else {
                           _controller.forward();
                       }
                  },
               ),
           );
        }
    
        @override
        void dispose() {
           _controller.dispose();
           super.dispose();
        }
    }
    
  • 3.2秒旋、Hero動畫
    移動端開發(fā)會經(jīng)常遇到類似這樣的需求:

    • 點擊一個頭像,顯示頭像的大圖诀拭,并且從原來圖像的Rect到大圖的Rect
    • 點擊一個商品的圖片迁筛,可以展示商品的大圖,并且從原來圖像的Rect到大圖的Rect
      這種跨頁面共享的動畫被稱之為享元動畫(Shared Element Transition)

    在Flutter中耕挨,有一個專門的Widget可以來實現(xiàn)這種動畫效果:Hero
    實現(xiàn)Hero動畫细卧,需要如下步驟:

    • 1.在第一個Page1中,定義一個起始的Hero Widget筒占,被稱之為source hero贪庙,并且綁定一個tag;
    • 2.在第二個Page2中翰苫,定義一個終點的Hero Widget止邮,被稱之為 destination hero这橙,并且綁定相同的tag;
    • 3.可以通過Navigator來實現(xiàn)第一個頁面Page1到第二個頁面Page2的跳轉過程导披;

    Flutter會設置Tween來界定Hero從起點到終端的大小和位置屈扎,并且在圖層上執(zhí)行動畫效果。
    首頁Page的核心代碼:

    GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        crossAxisSpacing: 6,
        mainAxisSpacing: 6,
        childAspectRatio: 16/9
      ),
      children: List.generate(20, (index) {
        String imageURL = "https://picsum.photos/200/300?random=$index";
        return GestureDetector(
          onTap: () {
            Navigator.of(context).push(PageRouteBuilder(
              pageBuilder: (ctx, animation1, animation2) {
                return FadeTransition(
                  opacity: animation1,
                  child: JKImageDeyail(imageURL),
                );
              },
            ));
          },
          child: Hero(tag: imageURL, child: Image.network(imageURL, fit: BoxFit.cover,)),
        );
      }),
    ),
    

    提示:外層包裹了一個:手勢 GestureDetector撩匕,跳轉用的帶動畫的 PageRouteBuilder鹰晨,對于跳轉的頁面包裹了漸變 FadeTransition
    對于展示的 Image 我們包裹了一個 Hero ,對于 Hero 下個頁面也要有 Hero止毕,并且和當前的 Hero 的 tag 保持一致

    圖片展示Page

    import 'package:flutter/material.dart';
    
    class JKImageDeyail extends StatelessWidget {
    
        final String _imageUrl;
    
        JKImageDeyail(this._imageUrl);
    
        @override
        Widget build(BuildContext context) {
           return Scaffold(
              backgroundColor: Colors.black,
              appBar: AppBar(
                 title: Text('圖片詳情'),
              ),
              body: Center(
                 child: GestureDetector(
                    onTap: () {
                      Navigator.of(context).pop();
                    },
                   child: Hero(
                     tag: _imageUrl,
                     child: Image.network(
                        _imageUrl,
                        width: double.infinity,
                        fit: BoxFit.cover,
                     ),
                   ),
                 ),
              ),
          );
       }
    }
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末模蜡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子滓技,更是在濱河造成了極大的恐慌哩牍,老刑警劉巖棚潦,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件令漂,死亡現(xiàn)場離奇詭異,居然都是意外死亡丸边,警方通過查閱死者的電腦和手機叠必,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來妹窖,“玉大人纬朝,你說我怎么就攤上這事〗竞簦” “怎么了共苛?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜓萄。 經(jīng)常有香客問我隅茎,道長,這世上最難降的妖魔是什么嫉沽? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任辟犀,我火速辦了婚禮,結果婚禮上绸硕,老公的妹妹穿的比我還像新娘堂竟。我一直安慰自己,他們只是感情好玻佩,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布出嘹。 她就那樣靜靜地躺著,像睡著了一般咬崔。 火紅的嫁衣襯著肌膚如雪税稼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音娶聘,去河邊找鬼闻镶。 笑死奋早,一個胖子當著我的面吹牛弧械,可吹牛的內容都是我干的。 我是一名探鬼主播顶掉,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狡耻,長吁一口氣:“原來是場噩夢啊……” “哼墩剖!你這毒婦竟也來了?” 一聲冷哼從身側響起夷狰,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤岭皂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沼头,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體爷绘,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年进倍,在試婚紗的時候發(fā)現(xiàn)自己被綠了土至。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡猾昆,死狀恐怖陶因,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情垂蜗,我是刑警寧澤楷扬,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站贴见,受9級特大地震影響烘苹,放射性物質發(fā)生泄漏。R本人自食惡果不足惜蝇刀,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一螟加、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧吞琐,春花似錦捆探、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至奴烙,卻和暖如春助被,著一層夾襖步出監(jiān)牢的瞬間剖张,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工揩环, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留搔弄,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓丰滑,卻偏偏與公主長得像顾犹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子褒墨,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354