Flutter动画基础教程

342 阅读7分钟

前言

关于Flutter中动画我们首先需要有一个大致的概念,就是Flutter提供一套用于控制Widget如何展示的基础类,这些基础类实际上和Widget并没有任何关系,它们只是给Widget提供数值用于赋值给它的属性。在这个大背景下我们来谈谈几个比较关键的几个用于展示动画的核心类.

播放动画的几个核心类说明

Animation

动画播放最核心的类,它知道当前动画播放的状态(开始,停止,前播放或者倒放),但是它不关心屏幕上显示的内容.

AnimationController

AnimationController是一个Animation对象,它在一定的时间生成0.0~1.0值.

例如


controller =

    AnimationController(duration: const Duration(seconds: 2), vsync: this);

CurvedAnimation

CurvedAnimation也是一个Animation对象,定义动画非线性的过程

Tween

这个称为补间动画,用于插入动画内容实际的值,例如颜色变化,从红色到蓝色. 或者值的变化0到255. 举个使用Tween例子, 下面在500ms中生成一个Integer值从0-255


AnimationController controller = AnimationController(

    duration: const Duration(milliseconds: 500), vsync: this);

Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

下面这个例子使用controller, curve和Tween


AnimationController controller = AnimationController(

    duration: const Duration(milliseconds: 500), vsync: this);

final Animation<double> curve =

    CurvedAnimation(parent: controller, curve: Curves.easeOut);

Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

这里我们看出来controller, curve和Tween实际上是一个组合结构,我们最终使用IntTween进行动画播放. controller提供了动画播放的时间,curve提供了动画数值的变化曲线,Tween则是提供具体的数值用于赋值到Widget上。

Listener和StatusListener

用于监听动画的状态的变化. 使用addListener()添加一个Listener,同于接收动画变化值的更新,最常的用处就是调用setState去刷新ui. StatusListener通过addStatusListener()添加,用于接收动画状态变化的通知,例如开始,结束正播活倒播.

用五个Demo来讲解动画的播放

Demo 1 了解Animation的基本实现方式

这个demo主要演示动画的渲染方式。实际在使用Animation类中我们不会做渲染操作,只是读取里面的状态值提供给widget进行渲染.

下面是一个没有动画效果的Flutter logo

import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
  const LogoApp({super.key});
  @override
  State<LogoApp> createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: const FlutterLogo(),
      ),
    );
  }
}

下面的代码展示了一个logo从无到最大的动画效果,我们定义了一个AnimationController, 需要传入一个vsync对象。

class _LogoAppState extends State<LogoAppV1> with SingleTickerProviderStateMixin{

  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {
          // 当animtaion值改编后开始刷新widget tree
        });
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

这里调用了一个addListener方法调用setState, 所以每次Animation生成一个新的数,当前的帧被标志为过期,这样强制调用build(), 在重新build过程中由于container的height和width被animation的value替代,所大小会变。 在dispose回调中还需要记得调用controller的dispose()避免内存泄露.

Demo 2 使用AnimatedWidget简化动画的实现

AnimationWidget允许你分开widget和animation的代码,AnimatedWidget不需要维护一个state对象去管理animation. 我们通过增加一个AnimatedLogo去实现demo 1同样的效果.

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

这里我们仍然需要创建AnimationController,Tween,作为参数传给AnimatedLogo。这样我们实现了widget和animation的代码的分离,更为清晰。

class _LogoAppState extends State<LogoAppV2> with SingleTickerProviderStateMixin{

  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);
}

这里我们与demo1比较,可以看出我们没有去实现addListener, 刷新widget tree, 而是用过AnimatedWidget帮我自动完成了这个操作.

Demo 3 监听动画的进度

通过增加addStatusListener()监听动画播放状态的改变, 例如开始,停止等。通常在动画播放过程中我们有这样需要知道动画状态情况的需要. 我们按照如下方式添加addStatusListener()监听动画播放状态.

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) => print('$status'));
    controller.forward();
  }
  // ...
}

运行以下代码输出

AnimationStatus.forward
AnimationStatus.completed

下面我们利用addStatusListener在动画播放完成后去反转一个动画

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {            
           if (status == AnimationStatus.completed) {            
                controller.reverse();            
           } else if (status == AnimationStatus.dismissed) {            
                controller.forward();            
           }
    controller.forward();
  }
  // ...
}

Demo 4 使用AnimatedBuilder重构动画

在Demo3中存在的一个问题是,改变动画需要改变渲染Logo的widget. 一个更好的解决方式是分开责任到不同的类。

包括以下三个责任:

  • 渲染logo
  • 定义Animation对象
  • 渲染转换过程

职责的区分可以通过AnimatedBuilder完成,一个AnimatedBuilder在渲染树中是一个分离类,就和

AnimationWidget一样。AnimatedBuilder自动监听Animation对象的通知,然后在必要的时候更新widget

tree. 所以不需要再调用addListener().

对于这个demo的 widget tree 大概如图

image.png

下面我们来看看最底部的LogoWidget的实现:

class LogoWidget extends StatelessWidget {
  const LogoWidget({super.key});

  // 省略高度和宽度属性这样可以方便填满parent.
  
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: const FlutterLogo(),
    );
  }
}

在图表中间的三块代码都在GrowTransition的build()中创建,GrowTransition它本身是一个stateless widget, 它持有一些必要的变量用于定义转换动画. build()方法创建和返回一个AnimatedBuilder, LogoWidget作为参数传入,AnimatedBuilder来控制LogoWidget的伸缩. 不多说了,看看代码

class GrowTransition extends StatelessWidget {
  const GrowTransition(
      {required this.child, required this.animation, super.key});

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

最后,这个demo的最终代码看起来和demo2有点类似,在initState()方法中创建一个AnimationController 和 Tween,然后通过animate()方法绑定。神奇的事情发生在build()方法中,它返回一个GrowTransition伴随一个LogoWidget作为一个child,一个animation对象去驱动这个转换过程.

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(
      animation: animation,
      child: const LogoWidget(),
    );
  }

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

PS: 虽然说这里的代码和demo2比较相似,但是它们有本质的区别demo2中实际上是使用AnimationWidget分开

widget和animation的代码,widget还是要处理转换过程,demo4更进一步隔离,把widget,过度和animation都

区分开了.

Demo 5 同时播放多个animation

有的时候我们希望同时一个播放一个大小变化和一个透明度变化的动画。这种情况我们可以通过创建两个Tween来实现这种效果.

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);
sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller);
opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);

在这里你可以通过sizeAnimation.value获取size和opacityAnimation.value获取opacity。这里我们利用

AnimatedWidget来实现。不过AnimatedWidget它只能传一个Animation作为参数,所以我们要略做改造,在AnimatedLogo中定义自己的Tween. 代码如下

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: const EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(),
        ),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

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

总结

以上通过5个demo演示了一些flutter动画的一些基本操作的,这实际上是一个循序渐进的过程,demo1中我们演示了AnimationController和Tween的结合使用,通过添加addListener去更新widget tree.但是这样做实际上耦合比较严重. 于是在demo2中我们使用AnimatedWidget进行了解耦。在demo3中演示了addStatusListener的使用,这个方法比较重要,我们通常需要监听动画的开始和结束操作,demo4中使用AnimatedBuilder实现动画,相对于demo2进一步结构化widget,transition和animation.实际上Flutter提供了很多开箱即用的过度动画FadeTransition, SizeTransition和SlideTransition,都是基于AnimatedBuilder方式实现的。最后一个demo5演示了动画同时播放的情况.