Flutter 動畫

Flutter動畫中用到的基本概念

Flutter動畫中有4個比較重要的角色:Animation、Controller、Curve、Tween,先來了解一下這四個角色

1.1 Animation

Animation是Flutter動畫庫中的核心類,用於插入指導動畫的值

Animation對象知道動畫當前的狀態(比如開始還是停止),可以使用addListener和addStatusListener監聽動畫狀態改變。

   animation.addListener((){
      //調用setState來刷新界面
      setState(() {
      });
    });
    animation.addStatusListener((status){
      debugPrint('status $status');
      switch (status){
        //動畫一開始就停止了
        case AnimationStatus.dismissed:
          break;
        //動畫從頭到尾都在播放
        case AnimationStatus.forward:
          break;
        //動畫從結束到開始倒着播放
        case AnimationStatus.reverse:
          break;
        //動畫播放完停止
        case AnimationStatus.completed:
          break;
      }
    });
  1. addListener: 每一幀都會調用,調用之後一般使用setState來刷新界面
  2. addStatusListener:監聽動畫當前的狀態 如動畫開始、結束、正向或反向

在Flutter中,Animation對象本身和UI渲染沒有任何關係。Animation是一個抽象類,它擁有其當前值和狀態(完成或停止)。其中一個比較常用的Animation類是Animation<double>,還可以生成除double之外的其他類型值,如:Animation<Color>Animation<Size>

1.2 AnimationController

用來管理Animation,它繼承自Animation,是個特殊的Animation,屏幕每刷新一幀,它都會生成一個新值,需要一個vsync參數,vsync的存在可以防止後臺動畫消耗不必要的資源

vsync的值怎麼獲得,可以讓stateful對象擴展使用TickerProviderStateMixin比如:

class AnimationDemoHome extends StatefulWidget {
  @override
  _AnimationDemoHomeState createState() => _AnimationDemoHomeState();
}

class _AnimationDemoHomeState extends State<AnimationDemoHome> with TickerProviderStateMixin{...}

AnimationController在默認情況下,在給定的時間段內,AnimationController會生成0.0到1.0的數字。

它可以控制動畫,比如使用.forward()方法可以啓動一個動畫,.stop()可以結束一個動畫,.reverse()啓動反向動畫。

  AnimationController({
    double value,
    this.duration,
    this.reverseDuration,
    this.debugLabel,
    this.lowerBound = 0.0,
    this.upperBound = 1.0,
    this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  }) 

看一下AnimationController的構造方法,有一個必須的參數TickerProvider,就是前面給定的TickerProviderStateMixin

在StatefulWidget中創建一個AnimationController對象

  animationController = AnimationController(
//      lowerBound: 32.0,
//      upperBound: 100.0,
      duration: Duration(milliseconds: 2000),
      vsync: this
    );

1.3 CurvedAnimation

定義動畫曲線,運動過程,比如勻速,先加速在減速等等

 CurvedAnimation({
    @required this.parent,
    @required this.curve,
    this.reverseCurve,
  })

它有兩個必要的參數parent和curve。parent就是前面的AnimationController對象,curve就是動畫運行的曲線,相當於Android屬性動畫中的插值器curve都有哪些取值呢

curve曲線 動畫過程
linear 勻速的
decelerate 勻減速
ease 先加速後減速
easeIn 開始慢後面快
easeOut 開始快後面慢
easeInOut 先慢在快在慢

上面是常用的一些曲線,還有很多中曲線運動的方式可以去curve.dart源碼中去看,源碼註釋中有mp4的鏈接,可以清楚的看到動畫運動的視頻。

abstract class Curve {
  const Curve();

  double transform(double t) {
    assert(t >= 0.0 && t <= 1.0);
    if (t == 0.0 || t == 1.0) {
      return t;
    }
    return transformInternal(t);
  }

  @protected
  double transformInternal(double t) {
    throw UnimplementedError();
  }
  ...
}

如果系統提供的運動曲線仍然無法滿足我們的需求,那就可以繼承Curve來自己實現一個。上面的代碼可以看到Curve是一個抽象類,繼承它並重寫transform方法即可。比如我們可以自己在裏面實現一個sin或者cos函數的曲線。例如

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

創建一個CurvedAnimation對象

CurvedAnimation curvedAnimation =
CurvedAnimation(parent: animationController,curve: Curves.bounceOut);

1.4 Tween:

給動畫對象插入一個範圍值

默認情況下,AnimationController對象的範圍從0.0到1.0,如果我們想要更大的範圍,就需要使用到Tween了。比如

Tween tween = Tween(begin: 32.0,end: 100.0);

class Tween<T extends dynamic> extends Animatable<T>Tween繼承自Animatable,接收一個begin和一個end值,Tween的職責就是定義從輸入範圍到輸出範圍的映射。所以這兩個值必須能進行加減乘的運算。

要使用Tween對象,調用其animate()方法,傳入一個控制器對象,返回一個Animation對象。例如,

Animation   animation = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
Animation   animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);

動畫的使用

2.1 Animation動畫

動畫的四個角色都瞭解了,下面開始使用這些角色來構建一個動畫,動畫效果如下圖

在這裏插入圖片描述

有一個心形的button,點擊的時候放大並且顏色漸變,在點擊的時候原路返回

class AnimateDemo1 extends StatefulWidget {
  @override
  _AnimateDemo1State createState() => _AnimateDemo1State();
}

class _AnimateDemo1State extends State<AnimateDemo1> with SingleTickerProviderStateMixin{
  AnimationController animationController;
  Animation animationSize;
  Animation animationColor;
  CurvedAnimation curvedAnimation;

  //Tween sizeTween;
  //Tween colorTween;
  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        vsync: this
    );
    //設置插值器  這裏使用一個默認的插值器bounceInOut
    curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
    animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
    animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
    animationController.addListener((){
      //刷新界面
      setState(() {});
    });
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: IconButton(
        icon: Icon(Icons.favorite),
        iconSize: animationSize.value,
        color: animationColor.value,
        //iconSize: sizeTween.evaluate(curvedAnimation),
        //color: colorTween.evaluate(curvedAnimation),
        onPressed: (){
          switch(animationController.status){
            case AnimationStatus.completed:
              animationController.reverse();
              break;
            default:
              animationController.forward();
          }
        },
      ),
    );
  }
}
  • 通過animation.value可以拿到動畫當前的值,然後賦值給當前需要動畫的控件的相關屬性即可
  • 需要在addListener中調用setState來刷新界面,否則沒效果
  • 需要注意 animationController需要在dispose()頁面銷燬的時候釋動畫資源。
  • 如果沒有調用Tween的animate方法來構建一個Animation,可以在使用的地方使用如上面代碼中sizeTween.evaluate(curvedAnimation)的方式來獲取當前值。

2.2使用AnimatedWidget

2.1中每次寫動畫都需要在addListener中設置setState來更新UI,有點麻煩,系統給提供了一個AnimatedWidget,它內部封裝了addListener和setState的邏輯,我們只需要傳給它AnimationController和Animation就行了。

而且我們可以自定義一個Widget繼承它,讓動畫跟原來的視圖代碼分離

class AnimationDemo2 extends StatefulWidget {
  @override
  _AnimationDemo2State createState() => _AnimationDemo2State();
}

class _AnimationDemo2State extends State<AnimationDemo2> with SingleTickerProviderStateMixin{

  AnimationController animationController;
  Animation animationSize;
  Animation animationColor;
  CurvedAnimation curvedAnimation;
  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      duration: Duration(milliseconds: 1000),
      vsync: this
    );
    //設置插值器  這裏使用一個默認的插值器bounceInOut
    curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
    animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
    animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimationHeart(
         animations: [
           animationSize,animationColor
         ],
        controller: animationController,
      ),
    );
  }
}
//動畫代碼抽離
class AnimationHeart extends AnimatedWidget{
  AnimationController controller;
  List animations;
  AnimationHeart({ this.animations,
    this.controller,}):super(listenable:controller);

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(Icons.favorite),
      iconSize: animations[0].value,
      color: animations[1].value,
      onPressed: (){
        switch(controller.status){
          case AnimationStatus.completed:
            controller.reverse();
            break;
          default:
            controller.forward();
        }
      },
    );
  }
}

自定義一個AnimationHeart繼承自AnimatedWidget,在構造方法中將AnimationController和Animation傳過來。其餘的跟2.1中一樣,最終效果也一樣。

2.3使用AnimatedBuilder

Flutter中還可以使用AnimatedBuilder來構建一個動畫

class AnimateDemo3 extends StatefulWidget {
  @override
  _AnimateDemo3State createState() => _AnimateDemo3State();
}

class _AnimateDemo3State extends State<AnimateDemo3> with SingleTickerProviderStateMixin{

  AnimationController animationController;
  Animation animationSize;
  Animation animationColor;
  CurvedAnimation curvedAnimation;
  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
        duration: Duration(milliseconds: 1000),
        vsync: this
    );
    //設置插值器  這裏使用一個默認的插值器bounceInOut
    curvedAnimation = CurvedAnimation(parent: animationController,curve: Curves.bounceOut);
    animationSize = Tween(begin: 32.0,end: 100.0).animate(curvedAnimation);
    animationColor = ColorTween(begin: Colors.red,end: Colors.green).animate(curvedAnimation);
  }

  @override
  void dispose() {
    super.dispose();
    animationController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animationController,
      builder: (context,child){
        return Center(
          child: IconButton(
            icon: Icon(Icons.favorite),
            iconSize: animationSize.value,
            color: animationColor.value,
            onPressed: (){
              switch(animationController.status){
                case AnimationStatus.completed:
                  animationController.reverse();
                  break;
                default:
                  animationController.forward();
              }
            },
          ),
        );
      },
    );
  }
}

實例化四個動畫元素的代碼跟前面還是一樣,主要是在build代碼塊中使用AnimatedBuilder構建,傳入animation對象。看起來比2.2中的方式也沒有簡單多少,不過看一下它的構造方法,系統還給提供了一個可選的參數child,讓它天然就支持封裝。

const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  })
  • 必需要一個Listenable,Animation就是Listenable
  • 必需要一個builder,前面的代碼中知道builder中需要傳一個context和一個child
  • 可以傳一個child。傳入的這個child最終會傳入到builder中

上面的例子中我們是直接在builder中創建了一個控件,既然child可以傳進來,那麼我們可以把一個類型的動畫封裝一下比如縮放動畫,漸變動畫等,以後只要把需要此動畫的小部件傳進來,這個小部件就有這個動畫了。

比如下面定義一個可以縮放的小部件。

class ScaleAnimate extends StatelessWidget {
  final Animation animation;
  final Widget child;
  ScaleAnimate({@required this.animation,@required this.child});
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: animation,
      builder: (context,child){
        return SizedBox(
            width: animation.value,
            height: animation.value,
            child: child,
          );
      },
      child: child,
    );
  }
}

Hero動畫

Hero動畫很簡單不過在平時的項目中也經常用到,主要用在路由頁面之間切換。比如一個頭像點擊看大圖,或者新聞列表頁面,點擊看詳情,這種共享式的無縫切換。

動畫效果如下圖

在這裏插入圖片描述

class AnimateDemo4 extends StatefulWidget {
  @override
  _AnimateDemo4State createState() => _AnimateDemo4State();
}

class _AnimateDemo4State extends State<AnimateDemo4> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: InkWell(
        child: Hero(
          tag: "avator",
          child: ClipOval(
            child: Image.network('http://ww1.sinaimg.cn/large/0065oQSqly1fsfq1k9cb5j30sg0y7q61.jpg',width: 100,),
          ),
        ),
        onTap: (){
          Navigator.of(context).push(MaterialPageRoute(builder: (context){
            return Scaffold(
              body: Center(
                child: Hero(
                  tag: "avator",
                  child: Image.network('http://ww1.sinaimg.cn/large/0065oQSqly1fsfq1k9cb5j30sg0y7q61.jpg'),
                ),
              ),
            );
          }));
        },
      ),
    );
  }
}
  • 當前頁面的圓形小圖和詳情頁面的大圖都使用Hero包裹。
  • 必須使用相同的tag,Flutter Framework通過tag來確定他們之間的關係。

交織動畫

有時候我們需要實現一組複雜的動畫,比如在0.1-0.2秒縮放,從0.2-0.4秒顏色漸變,從0.4-0.8秒左右移動,這時候使用交織動畫可以方便的完成,使用交織動畫需要注意下面幾點

  • 需要使用多個Animation對象
  • 一個AnimationController控制所有的動畫對象
  • 給每一個動畫對象指定時間間隔(Interval)

在這裏插入圖片描述

class AnimateDemo5 extends StatefulWidget {
  @override
  _AnimateDemo5State createState() => _AnimateDemo5State();
}

class _AnimateDemo5State extends State<AnimateDemo5> with TickerProviderStateMixin{
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        duration: const Duration(milliseconds: 2000),
        vsync: this
    );
  }
    @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          SizedBox(height: 30,),
          Center(
            child: StaggerAnimation(controller: _controller,),
          ),
          SizedBox(height: 30,),
          RaisedButton(
            child: Text("點擊開始"),
            onPressed: () {
              _play();
            },
            textColor: Theme.of(context).primaryColor,
            splashColor: Colors.grey[400],
          )
        ],
      ),
    );
  }

  void _play() async{
    //先正向執行動畫
    await _controller.forward().orCancel;
    //再反向執行動畫
    await _controller.reverse().orCancel;
  }
}

class StaggerAnimation extends StatelessWidget {
  final AnimationController controller;
  Animation<double> width,height;
  Animation<EdgeInsets> padding;
  Animation<Color> color;
  Animation<BorderRadius> borderRadius;

  StaggerAnimation({Key key,this.controller}): super(key:key){
    height = Tween<double>(
        begin: 0,
        end: 200)
        .animate(CurvedAnimation(parent: controller, curve: Interval(0.0,0.4,curve: Curves.ease)));
    width = Tween<double>(
        begin: 50,
        end: 200)
        .animate(CurvedAnimation(parent: controller, curve: Interval(0.0,0.4,curve: Curves.ease)));
    padding = Tween<EdgeInsets>(
      begin:EdgeInsets.only(left: .0),
      end:EdgeInsets.only(left: 100.0),
    ).animate(CurvedAnimation(parent: controller, curve: Interval(0.6, 1.0, curve: Curves.ease)),);
    color = ColorTween(
      begin:Colors.green ,
      end:Colors.red,
    ).animate(CurvedAnimation(parent: controller, curve: Interval(0.0, 0.4, curve: Curves.ease,)));
    borderRadius = BorderRadiusTween(
      begin: BorderRadius.circular(3),
      end: BorderRadius.circular(35),
    ).animate(CurvedAnimation(parent: controller, curve: Interval(0.4, 0.6,curve: Curves.ease,),));
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context,child){
        return Container(
          alignment: Alignment.bottomCenter,
          padding:padding.value ,
          child: Container(
            width: width.value,
            height: height.value,
            decoration: BoxDecoration(
                color: color.value,
                border: Border.all(color: Colors.blue,width: 3),
                borderRadius:borderRadius.value
            ),
          ),
        );
      },
    );
  }
}
  • StaggerAnimation中定義了5個動畫,寬,高,顏色,左邊距,圓角
  • 使用Interval來定義某個動畫執行的時機
  • 最後異步啓動動畫。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章