在Flutter中的动画分为补间(Tween)动画和基于物理(Physics-based)的动画,由于篇幅原因我们今天就先来看下补间动画,当然也是我们接触比较多的动画类型。

补间动画的基本支持类

在Flutter中Animation对象是Flutter动画库中的一个核心类,它生成指导动画的值,没错它仅仅用来生成动画的值,这些值并不会直接没改变界面的展示效果。

Animation对象知道动画的当前状态(例如,它是开始、停止还是向前或向后移动),但它不知道屏幕上显示的内容。

在Flutter中我们使用AnimationController来管理动画,控制动画的开始、结束与快慢。

CurvedAnimation 可以说是动画的插值器,负责控制动画的行为,如是先快再慢还是先快再慢等。

入门补间动画

Animation在Flutter中是一个抽象类,我们并不能直接来是使用它,但是我们可以使用Tween这个子类来使用它。

我们可以使用addListener回调来监听动画值的改变,并调用setState()方法,来触发UI重建,可以使用addStatusListener回调来监听动画状态的变更

刚刚我们说过,使用Animation并不能直接改变Widget,它只能生成一系列的值,那么到底是不是这样呢?我们还是看个例子

每次我们点击floatingActionButton都会触发动画开始的操作,然后通过监听把当前动画的值打印到控制台上。

import 'package:flutter/material.dart';

void main() {
    runApp(new MaterialApp(home: new MyApp(),));
}

class MyApp extends StatefulWidget {
    @override
    State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
    Animation<double> doubleAnimation;
    AnimationController animationController;
    String _showTween;

    @override
    Widget build(BuildContext context) {
        return Scaffold(appBar: AppBar(title: Text("AnimAtion"),),
            floatingActionButton: FloatingActionButton(
                child: (_showTween == null) ? Icon(Icons.add) : Text(
                        _showTween),
                onPressed: onAnimationStart,
            ),
        );
    }

    @override
    void initState() {
        super.initState();
        animationController = AnimationController(
                vsync: this, duration: const Duration(milliseconds: 2000));
        doubleAnimation =
        Tween(begin: 0.0, end: 100.0).animate(animationController)
            ..addListener(() {
                setState(() {
                    _showTween = doubleAnimation.value.toInt().toString();
                });
                print(_showTween);
            });
    }

    onAnimationStart() {
        animationController.forward(from: 0.0);
    }

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

上面的代码很简单,我们在Widget初始化时建立了一个AnimationController对象用来控制动画的播放,并设置动画时长为2秒

然后我们建立一个Tween动画,从0.0开始到100.0结束,并且给动画设置监听,动画的值改变时都会触发print和setState方法,把当前的值打印在控制台上并显示到floatingActionButton上。

I/flutter ( 1664): 0
I/flutter ( 1664): 0
I/flutter ( 1664): 3
I/flutter ( 1664): 4
I/flutter ( 1664): 4
I/flutter ( 1664): 5
......
I/flutter ( 1664): 100

AnimatedWidget

在上面的例子中我们必须需要通过在上面的例子中我们必须需要通过addListener()和setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,并允许我们将Widget分离出来,重构后的代码如下:

import 'package:flutter/material.dart';

void main() {
    runApp(MaterialApp(
        home: MyApp(),
    ));
}

class MyApp extends StatefulWidget {
    @override
    State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
    Animation<double> numberAnimation;
    AnimationController controller;

    @override
    void initState() {
        super.initState();
        controller = AnimationController(
            duration: const Duration(milliseconds: 2000),
            vsync: this,
        );
        numberAnimation = Tween(begin: 0.0, end: 100.0).animate(controller);
    }


    void onAnimationStart() {
        controller.forward(from: 0.0);
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text("Animation"),
            ),
            floatingActionButton: new FloatingActionButton(
                onPressed: onAnimationStart,
                child: AnimationText(animation: numberAnimation,),
            ),
        );
    }

    dispose() {
        controller.dispose();
        super.dispose();
    }
}

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

    @override
    Widget build(BuildContext context) {
        final Animation<double> animation = listenable;
        return Text(animation.value.toInt().toString());
    }
}

其实使用起来也非常的简单,只不过我们自定义了一个AnimationText继承于AnimatedWidget来获得对动画的监听并给Text赋值,当然程序的运行效果跟上面的例子是一样的。

控制Image的大小

在上面的例子中我们通过动画的值来改变了Text显示的值,现在我们来试下改变Image的大小。

其实实现起来很简单,只是把动画的值赋值给Widget的宽和高而已!

代码如下:

import 'package:flutter/material.dart';

void main() {
    runApp(MaterialApp(
        home: MyApp(),
    ));
}

class MyApp extends StatefulWidget {
    @override
    State<StatefulWidget> createState() => MyAppState();
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
    Animation<double> numberAnimation;
    AnimationController controller;
    bool _running = false;
    @override
    void initState() {
        super.initState();
        controller = AnimationController(
            duration: const Duration(milliseconds: 2000),
            vsync: this,
        );
        numberAnimation = Tween(begin: 0.0, end: 200.0).animate(controller)..addStatusListener((status){
            if(status == AnimationStatus.forward){
                print("动画开始");
            } else if(status == AnimationStatus.completed){
                print("动画结束");
                controller.reverse();
            } else if (status == AnimationStatus.dismissed) {
                print("动画消失");
                controller.forward();
            }
        });
    }


    void onAnimationStart() {
        if(_running){
            controller.stop();
        }else{
            controller.forward(from: 0.0);
        }
        setState(() {
            _running = !_running;
        });
    }

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
                title: Text("Animation"),
            ),
            body: AnimationImage(animation: numberAnimation),
            floatingActionButton: FloatingActionButton(
                onPressed: onAnimationStart,
                child: (_running) ? Icon(Icons.stop) : Icon(Icons.play_circle_outline),
            ),
        );
    }

    void dispose() {
        controller.dispose();
        super.dispose();
    }
}

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

    @override
    Widget build(BuildContext context) {
        final Animation<double> animation = listenable;
        return Center(
            child: Image.asset('assets/logo.png', width: animation.value, height: animation.value,),
        );
    }
}

注意:上面我不仅仅改变了动画的大小,我们还在监听动画的状态变更,当动画结束时我们反转动画,当动画的反转也结束后我们从新开始动画,这样动画就会一直这样循环下去。

CurvedAnimation(曲线动画)

接下来我们来看下CurvedAnimation,通过CurvedAnimation我们可以实现动画的非线性播放,比如先快后慢或者弹性之类的!

各种AnimationWidgets

在Flutter内部为们提供了很多的动画Wdiget,比如AnimatedContainer、AnimatedCrossFade、AnimatedOpacity、AnimatedSize、AnimatedPositioned等等。

AnimatedContainer

AnimatedContainer看名字不难知道它是Container的动画版本,使用AnimatedContainer就不需要再创建Animation了。

AnimatedContainer继承于ImplicitlyAnimatedWidget,相应的我们需要传入动画的播放时间(必须填写)和插值器类型(有默认值)

然后,我们就可以让AnimatedContainer根据我们的值来做动画效果并且作用到相应的属性上。

AnimatedOpacity

接下来我们来看下改变透明度的动画Widget,可以child根据设置的时间和动画范围改变透明度

同样的AnimatedOpacity也继承于ImplicitlyAnimatedWidget,需要填入Curve和Duration参数。

AnimatedCrossFade

AnimatedCrossFade允许一个Widget到另为一个Widget使用渐变的改变。

与 AnimatedOpacity和AnimatedContainer不同,AnimatedCrossFade直接继承于StatefulWidget,构造方法必须要传入两个不同的Widget,当然你也可以指定每个widget的动画差值器。

总结:

使用系统提供的AnimationWidgets可以很方便的实现Widget的动画效果,在做一些简单的动画时还是非常有用的,当然在flutter中还为我们提供很多其他的动画效果如AnimatedPositioned、AnimatedSize、SlideTransition、SizeTransition、ScaleTransition、RotationTransition、PivotTransition等动画效果,前两者与今天讲到的类似,其他的类似于上期的AnimatedWidget用法!

Hero动画

Hero指的是可以在路由(页面)之间“飞行”的widget,简单来说Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。

你可能多次看到过 hero 动画。例如,一个路由中显示待售商品的缩略图列表,选择一个条目会将其跳转到一个新路由,新路由中包含该商品的详细信息和“购买”按钮。 在Flutter中将图片从一个路由“飞”到另一个路由称为hero动画,尽管相同的动作有时也称为 共享元素转换。下面我们通过一个示例来体验一下hero 动画。

示例

假设有两个路由A和B,他们的内容交互如下:

A:包含一个用户头像,圆形,点击后跳到B路由,可以查看大图。

B:显示用户头像原图,矩形;

在AB两个路由之间跳转的时候,用户头像会逐渐过渡到目标路由页的头像上

import 'package:flutter/material.dart';
class HeroAvatarPage extends StatefulWidget {
  @override
  _HeroAvatarPageState createState() => _HeroAvatarPageState();
}

class _HeroAvatarPageState extends State<HeroAvatarPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text('Hero动画缩略图'),
        ),
        body: Container(
            alignment: Alignment.topCenter,
            child: InkWell(
                child: SizedBox(
                    child: Hero(
                        tag: 'avatar',
                        child: CircleAvatar(
                            //child: Image.asset('assets/avatar.jpg'),
                            backgroundColor: Colors.black,
                            backgroundImage: AssetImage('assets/avatar.jpg'),
                            radius: 30.0,
                        ),
                    ),
                    width: 100.0,
                    height: 100.0,
                ),
                onTap: (){
                    Navigator.of(context).push(PageRouteBuilder(
                        pageBuilder: (BuildContext context, Animation animation,
                            Animation secondaryAnimation){
                            /*return FadeTransition(
                                opacity: animation,
                                child: HeroAvatarDisplayPage(),
                            );*/
                            return HeroAvatarDisplayPage();
                        }
                    ));
                },
            ),
        ),
    );
  }
}


class HeroAvatarDisplayPage extends StatefulWidget {
  @override
  _HeroAvatarDisplayPageState createState() => _HeroAvatarDisplayPageState();
}

class _HeroAvatarDisplayPageState extends State<HeroAvatarDisplayPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: Text('展示大图'),
        ),
        body: InkWell(
            child: Center(
                child: Hero(
                    tag: 'avatar',
                    child: Image.asset('assets/avatar.jpg'),
                ),
            ),
            onTap: (){
                Navigator.of(context).pop();
            },
        ),
    );
  }
}