动画的原理以及Tween与Curve的使用

来源:blog.csdn.net 更新时间:2023-05-25 21:55
前言
不管是Android还是IOS,我们都能看到用户在使用动画,交互动画可以作为用户的操作向导,不让操作乏味无趣,可以说没有动画的APP,是没有灵魂的App。本篇将讲解在Flutter中如何使用。



动画的原理
提到动画原理,就不得不说“动画帧”。比如手机拍摄的视频,虽然看起来展现的是一个连续播放的视频,但其实视频本身都是由一幅幅静态的图片组成,静态图片的连续展示就是一段视频。

这里由一个概念,即画面每秒传输帧数,经常玩游戏的应该非常清楚就是FPS帧率,FPS越高,游戏越流畅,同样,动画也是如此。目前大多数设备的刷新频率为60Hz,所以在通常情况下FPS为60fps时效果最佳,也就是每秒消耗的时间为16.67ms。在Flutter开发中,FPS已经达到了60fps,所以动画的流畅性也已达到原生动画的效果。

当然,在Flutter开发中,动画的核心类也是Animation类,它可以判断当前的动画状态(比如开始,停止,移动,前进,反向),但是它不关心屏幕上显示的任何组件。Animation是由AnimationController管理的,并通过Listeners和StateListeners管理动画状态所发生的变化。在Flutter中,通过Animation,AnimationController,Tween,Curve对动画进行了抽象(模型化)。

Animation
在动画中,使用最多的类型是Animation。Animation对象本身对手机屏幕是无感知的,而且,它对调用的build方法也是无感知的。它是一个抽象类,仅仅知道当前的动画插值和状态(complete,dismissed)。Animation对象是一个在一段时间内,依次生成一个区间值的类,其输出值是线性的,非线性的,也可以是一个步进函数或者其他曲线函数。控制器可以决定Animation动画的运行方式:正向,反向以及在中间进行切换.Animation也可以生成除double之外的其他值,比如Animation或Animation。

Animatable
Animatable是控制动画类型的类。比如在平移动画中,我们关系的是(x,y)的值,那么这个时候就需要Animatable控制(x,y)的值的变化。在颜色动画中,我们关心的是色值的变化,那么就需要Animatable控制色值的变化。在贝塞尔曲线运动中,我们关心的是路径是否按照贝塞尔方程式来生成(x,y),所以Animatable就要按照贝塞尔方程式的方式来改变(x,y)。

AnimationController
AnimationController即动画控制器,它负责在给定的时间段内,以线性的方式生成默认区间为(0.0,1.0)的数字。我们可以通过AnimationController来创建Animation对象,例如:

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

以上AnimationController的创建派生自Animation,它能告诉Flutter动画的控制器已经创建好对象了,并且处于准备状态。要真正让动画运作起来,则需要通过AnimationController的forward()方法来启动动画。其中,数值的产生取决与屏幕的刷新情况,一般每秒60帧。数值生成以后,每个Animation对象都会通过Listener进行回调。

Tween
Tween是补间动画,我们上面已经讲过AnimationController的取值范围是(0.0,1.0),如果我们需要不同范围或类型的值,就可以使用Tween,比如:

Tween doubleTween=Tween<double>(begin:0.0,end:300.0);

可以看到Tween构造函数有两个参数begin,end。而它的作用就是定义从输入方位到输出范围的映射()。

Tween继承自Animatable,而不是Animation。虽然Animatable与Animation相似,但它不是必须输出double值,也可以输出ColorTween颜色值。

虽然Tween不会存储任何状态信息,但它给我们提供了evaluate(Animation animation)方法,并可以通过映射(反射吧?)获取动画当前值。Animation当前值可以通过value方法来获取。evaluate还能执行其他处理,比如确保动画值分别为0.0和1.0时,返回开始和结束状态。若要使用Tween对象,需要调用animate方法,并传入一个控制器对象。

Tween实现循环放大与缩小
通过上面的理论学习,我们基本掌握了Animation的使用方法,下面我们直接实现一个动画来:

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
  Animation<double> animation;
  AnimationController controller;

  initState(){
    super.initState();
    controller=AnimationController(duration: const Duration(milliseconds: 2000),vsync: this);
    animation=Tween(begin: 0.0,end: 300.0).animate(controller)..addStatusListener((status){
      if(status==AnimationStatus.completed){//动画在结束时停止的状态
        controller.reverse();//颠倒
      }else if(status==AnimationStatus.dismissed){//动画在开始时就停止的状态
        controller.forward();//向前
      }
    })..addListener((){
      setState(() {
      });
    });
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    // TODO: implement dispose
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        data-height: animation.value,
        margin: EdgeInsets.symmetric(vertical: 10.0),
        data-width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

代码很简单,我们先定义了控制器与animation动画,然后实例化控制器,这里我们混入了SingleTickerProviderStateMixin类,因为初始化animationController的时候需要一个TickerProvider类型的参数Vsync,所以我们混入了TickerProvider的子类SingleTickerProviderStateMixin。

接着我们通过控制器初始化一个 Animation 对象,设置它的开始为0,结束为300,因为这里必须返回一个animation对象我们才能调用监听函数,所以我们这里用了级联操作符"…",如果不用级联操作符,也可以这样写:

animation=Tween(begin: 0.0,end: 300.0).animate(controller)
animation.addStatusListener((status){
      if(status==AnimationStatus.completed){//动画在结束时停止的状态
        controller.reverse();//颠倒
      }else if(status==AnimationStatus.dismissed){//动画在开始时就停止的状态
        controller.forward();//向前
      }
    }).addListener((){
      setState(() {
        print(animation.value);
      });
    });

接着我们通过AnimationStatus.completed判断动画是否为结束状态, 如果是结束状态,我们让它reverse反向执行动画,同样,如果是AnimationStatus.dismissed动画在开始时停止,我们就forward正常执行动画,这样就能无限循环的执行动画。

..addListener((){
      setState(() {
      });

这里监听函数只写了setState,我们前面已经讲过,要更改组件的状态必须通过setState来改变,所以这里必须写入这个,不然组件是不会更改其状态的,当前你也可以通过animation.value获取执行到的当前值。

Curve
在Flutter开发中,通过Curve曲线来描述动画的过程,可以是线性的Curves.linear,也可以是非线性的ono-linear。因此,整个动画过程可以是匀速的,加速的,先加速后减速的等等都行,例如:

CurvedAnimation curve=CurvedAnimation(parent:controller,curve:curves.easeIn);

这里我们创建了一个先快后慢的曲线动画。下面我们定义一个震荡曲线的动画,可以看作弹弹球反过来的动画曲线:

Animation<double> animation;
  AnimationController controller;

  initState(){
    super.initState();
    controller=AnimationController(duration: const Duration(milliseconds: 2000),vsync: this);
    animation=CurvedAnimation(parent: controller,curve: Curves.bounceIn)..addStatusListener((status){
      if(status==AnimationStatus.completed){//动画在结束时停止的状态
        controller.reverse();//颠倒
      }else if(status==AnimationStatus.dismissed){//动画在开始时就停止的状态
        controller.forward();//向前
      }
    })..addListener((){
      setState(() {
        print(animation.value);
      });
    });
    controller.forward();
  }

上面的代码就修改了Tween那句,替换成了CurvedAnimation,这样我们的震荡弹性曲线动画就完成了,Curves.bounceIn可以看成先放大在缩小,在放大在缩小,在放大在缩小,在放大的过程,实现效果如本文首图所示。