Day14 - Flutter - 動畫

概述

  • 動畫API認(rèn)識
  • 動畫案例練習(xí)
  • 其它動畫補充
一、動畫API認(rèn)識

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

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

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

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

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

      • 最終,如果動畫到達其區(qū)間的結(jié)束點(例如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);
        
            // 獲取動畫當(dāng)前狀態(tài)
            AnimationStatus get status;
        
            // 獲取動畫當(dāng)前的值
            @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的回調(diào)(看下面詳細(xì)解析)
           @required TickerProvider vsync,
       })
    }
    
    • AnimationController有一個必傳的參數(shù)vsync,它是什么呢?
      • Flutter的渲染閉環(huán),F(xiàn)lutter每次渲染一幀畫面之前都需要等待一個vsync信號。
      • 這里也是為了監(jiān)聽vsync信號,當(dāng)Flutter開發(fā)的應(yīng)用程序不再接受同步信號時(比如鎖屏或退到后臺),那么繼續(xù)執(zhí)行動畫會消耗性能。
      • 這個時候我們設(shè)置了Ticker,就不會再出發(fā)動畫了。
      • 開發(fā)中比較常見的是將 SingleTickerProviderStateMixin 混入到State的定義中。
  • 1.3、CurvedAnimation(設(shè)置動畫執(zhí)行的速率-速率曲線)
    CurvedAnimation也是Animation的一個實現(xiàn)類,它的目的是為了給AnimationController增加動畫曲線:
    CurvedAnimation可以將AnimationControllerCurve結(jié)合起來,生成一個新的Animation對象

    class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
        CurvedAnimation({
            // 通常傳入一個AnimationController
            @requiredthis.parent,
            // Curve類型的對象
            @requiredthis.curve,
            this.reverseCurve,
        });
    }
    
    • Curve類型的對象的有一些常量Curves(和Color類型有一些Colors是一樣的),可以供我們直接使用:

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

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

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

    Tween也有一些子類,比如ColorTween、BorderTween,可以針對動畫或者邊框來設(shè)置動畫的值。
    Tween.animate
    要使用Tween對象,需要調(diào)用Tween的animate()方法,傳入一個Animation對象。

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

    • 點擊案例后執(zhí)行一個心跳動畫,可以反復(fù)執(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 設(shè)置值的范圍
              _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)聽動畫值的改變,并且改變后需要調(diào)用setState(也就是上面的第4步),這會帶來兩個問題:

    • 1.執(zhí)行動畫必須包含這部分代碼,代碼比較冗余
    • 2.調(diào)用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調(diào)用的地方直接:IconAnimation(_animation)

    • 缺點是:1、每次都需要創(chuàng)建一個類,類里面的build也會打??;2、如果創(chuàng)建的Widget有子類,那么子類依然會重復(fù)的build
  • 2.3、AnimatedBuilder(優(yōu)解)
    AnimatedBuilder 可以解決上面 AnimatedWidget 產(chǎn)生的兩個問題,代碼如下

    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.設(shè)置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í)行動畫
    動畫集合了透明度變化、大小變化、顏色變化、旋轉(zhuǎn)動畫等;
    我們這里是通過多個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.設(shè)置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的跳轉(zhuǎn)過程;

    Flutter會設(shè)置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,跳轉(zhuǎn)用的帶動畫的 PageRouteBuilder,對于跳轉(zhuǎn)的頁面包裹了漸變 FadeTransition
    對于展示的 Image 我們包裹了一個 Hero ,對于 Hero 下個頁面也要有 Hero,并且和當(dāng)前的 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,
                     ),
                   ),
                 ),
              ),
          );
       }
    }
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容