Flutter 之 动画 (三十三)

动画其实是我们通过某些方式(比如对象,Animation对象)给Flutter引擎提供不同的值,而Flutter可以根据我们提供的值,给对应的Widget添加顺滑的动画效果。

1. Animation

Animation是一个抽象类,它本身和UI渲染没有任何关系,而它主要的功能是保存动画的插值和状态;其中一个比较常用的Animation类是Animation<double>。Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,这由Curve来决定。 根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。Animation还可以生成除double之外的其他类型值,如:Animation<Color> 或Animation<Size>。在动画的每一帧中,我们可以通过Animation对象的value属性获取动画的当前状态值

动画通知

addListener方法
它可以用于给Animation添加帧监听器,在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建

addStatusListener
当动画的状态发生变化时,会通知所有通过 addStatusListener 添加的监听器。
通常情况下,动画会从 dismissed 状态开始,表示它处于变化区间的开始点。
举例来说,从 0.0 到 1.0 的动画在 dismissed 状态时的值应该是 0.0。
动画进行的下一状态可能是 forward(比如从 0.0 到 1.0)或者 reverse(比如从 1.0 到 0.0)。
最终,如果动画到达其区间的结束点(比如 1.0),则动画会变成 completed 状态。

动画状态 AnimationStatus

枚举值 含义
dismissed 动画在起始点停止
forward 动画正在正向执行
reverse 动画正在反向执行
completed 动画在终点停止

#

abstract class Animation<T> extends Listenable implements ValueListenable<T> {
  const Animation();
  
  // 添加动画监听器
  @override
  void addListener(VoidCallback listener);
  
  // 移除动画监听器
  @override
  void removeListener(VoidCallback listener);
    
  // 添加动画状态监听器
  void addStatusListener(AnimationStatusListener listener);
    
  // 移除动画状态监听器
  void removeStatusListener(AnimationStatusListener listener);
    
  // 获取动画当前状态
  AnimationStatus get status;
    
  // 获取动画当前的值
  @override
  T get value;

2. AnimationController

Animation是一个抽象类,并不能用来直接创建对象实现动画的使用。
AnimationController是Animation的一个子类,实现动画通常我们需要创建AnimationController对象。

AnimationController会生成一系列的值,默认情况下值是0.0到1.0区间的值;
除了上面的监听,获取动画的状态、值之外,AnimationController还提供了对动画的控制:

  • forward:向前执行动画
  • reverse:反向播放动画
  • stop:停止动画

AnimationController的源码:

  AnimationController({
    double? value, // 初始化值
    this.duration, // 动画执行的时间
    this.reverseDuration,// 反向动画执行的时间
    this.debugLabel, // 标识 调试使用
    this.lowerBound = 0.0, //  最小值
    this.upperBound = 1.0, //  最大值
    this.animationBehavior = AnimationBehavior.normal,
    required TickerProvider vsync, // 刷新率ticker的回调(看下面详细解析)
  })
final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 2000),
  vsync: this,
);

AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用。 但是,AnimationController具有控制动画的其他方法,例如forward()方法可以启动正向动画,reverse()可以启动反向动画。

在动画开始执行后开始生成动画帧,屏幕每刷新一次就是一个动画帧,在动画的每一帧,会随着根据动画的曲线来生成当前的动画值(Animation.value),然后根据当前的动画值去构建UI,当所有动画帧依次触发时,动画值会依次改变,所以构建的UI也会依次变化,所以最终我们可以看到一个完成的动画。 另外在动画的每一帧,Animation对象会调用其帧监听器,等动画状态发生改变时(如动画结束)会调用状态改变监听器。

vsync
AnimationController有一个必传的参数vsync,这是什么?

  • Flutter有自己的渲染闭环,Flutter每次渲染一帧画面之前都需要等待一个vsync信号。
  • 这里也是为了监听vsync信号,当Flutter开发的应用程序不再接受同步信号时(比如锁屏或退到后台),那么继续执行动画会消耗性能。这个时候我们设置了Ticker,就不会再出发动画了。
  • 开发中比较常见的是将SingleTickerProviderStateMixin混入到State的定义中。

duration
duration表示动画执行的时长,通过它我们可以控制动画的速度。

注意: 在某些情况下,动画值可能会超出AnimationController的[0.0,1.0]的范围,这取决于具体的曲线。例如,fling()函数可以根据我们手指滑动(甩出)的速度(velocity)、力量(force)等来模拟一个手指甩出动画,因此它的动画值可以在[0.0,1.0]范围之外 。也就是说,根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。

3. CurvedAnimation

动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。

CurvedAnimation也是Animation的一个实现类,它的目的是为了给AnimationController增加动画曲线

CurvedAnimation可以将AnimationController和Curve结合起来,生成一个新的Animation对象

class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  CurvedAnimation({
    // 通常传入一个AnimationController
    @required this.parent,
    // Curve类型的对象
    @required this.curve,
    this.reverseCurve,
  });
}
final CurvedAnimation curve =
    CurvedAnimation(parent: controller, curve: Curves.easeIn);

Curve类型的对象的有一些常量Curves(和Color类型有一些Colors是一样的),可以供我们直接使用:

常见的几种Curve

Curves曲线 动画过程
linear 匀速的
decelerate 匀减速
ease 开始加速,后面减速
easeIn 开始慢,后面快
easeOut 开始快,后面慢
easeInOut 开始慢,然后加速,最后再减速

Curve对应值的效果,可以直接查看官网(有对应的gif效果,一目了然)
https://api.flutter.dev/flutter/animation/Curves-class.html

自定义Curse

官方也给出了自定义Curse的一个示例:
正弦曲线 ShakeCurve

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

4. Ticker

当创建一个AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:

abstract class TickerProvider {
  //通过一个回调创建一个Ticker
  Ticker createTicker(TickerCallback onTick);
}

Flutter 应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发

通常我们会将SingleTickerProviderStateMixin添加到State的定义中,然后将State对象作为vsync的值。

class _MSBasicAnimationPageState extends State<MSBasicAnimationPage>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

     ...
  }
  ...
}

5. Tween

默认情况下,AnimationController对象值的范围是[0.0,1.0]。如果我们需要构建UI的动画值在不同的范围或不同的数据类型,则可以使用Tween来添加映射以生成不同的范围或数据类型的值

class Tween<T extends dynamic> extends Animatable<T> {
  Tween({ this.begin, this.end });
}

Tween构造函数需要begin和end两个参数。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为[0.0,1.0],但这不是必须的,我们可以自定义需要的范围。

例如,像下面示例,Tween生成[-200.0,0.0]的值:

final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);

Tween继承自Animatable<T>,而不是继承自Animation<T>,Animatable中主要定义动画值的映射规则。

Tween也有一些子类,比如ColorTween、BorderTween,可以针对动画或者边框来设置动画的值。

下面我们看一个ColorTween将动画输入范围映射为两种颜色值之间过渡输出的例子:

final Tween colorTween =
    ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween对象不存储任何状态,相反,它提供了evaluate(Animation<double> animation)方法,它可以获取动画当前映射值。 Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。

注意:同时设置了Tween 和AnimationController的lowerBound、upperBound,可能会崩溃。

Tween.animate

要使用 Tween 对象,需要调用其animate()方法,然后传入一个Animation对象。

注意 animate()返回的是一个Animation,而不是一个Animatable。

例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

以下示例构建了一个控制器、一条曲线和一个 Tween:

final AnimationController controller = AnimationController(
  duration: const Duration(milliseconds: 500), 
  vsync: this,
);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

6. lerp

动画的原理其实就是每一帧绘制不同的内容,一般都是指定起始和结束状态,然后在一段时间内从起始状态逐渐变为结束状态,而具体某一帧的状态值会根据动画的进度来算出,因此,Flutter 中给有可能会做动画的一些状态属性都定义了静态的 lerp 方法(线性插值),比如:

//a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);

lerp 的计算一般遵循: 返回值 = a + (b - a) * t,其它拥有 lerp 方法的类:

// Size.lerp(a, b, t)
// Rect.lerp(a, b, t)
// Offset.lerp(a, b, t)
// Decoration.lerp(a, b, t)
// Tween.lerp(t) //起始状态和终止状态在构建 Tween 的时候已经指定了
...

需要注意,lerp 是线性插值,意思是返回值和动画进度t是成一次函数(y = kx + b)关系,因为一次函数的图像是一条直线,所以叫线性插值。如果我们想让动画按照一个曲线来执行,我们可以对 t 进行映射,比如要实现匀加速效果,则 t' = at²+bt+c,然后指定加速度 a 和 b 即可(大多数情况下需保证 t' 的取值范围在[0,1],当然也有一些情况可能会超出该取值范围,比如弹簧(bounce)效果),而不同 Curve 可以按照不同曲线执行动画的的原理本质上就是对 t 按照不同映射公式进行映射。

7. 动画基本使用 - 示例


class MSBasicAniamtionRouter extends StatefulWidget {
  const MSBasicAniamtionRouter({Key? key}) : super(key: key);

  @override
  State<MSBasicAniamtionRouter> createState() => _MSBasicAniamtionRouterState();
}

class _MSBasicAniamtionRouterState extends State<MSBasicAniamtionRouter>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _curveAnimation;
  late Animation<double> _tweenAnimation;
  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));
    // 动画速率曲线
    _curveAnimation =
        CurvedAnimation(parent: _controller, curve: Curves.linear);
    // 动画执行的value范围
    _tweenAnimation = Tween(begin: 50.0, end: 150.0).animate(_curveAnimation);

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

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.dismissed) {
        // 向前执行动画
        _controller.forward();
      } else if (status == AnimationStatus.completed) {
        // 反向执行动画
        _controller.reverse();
      }
    });

    // 向前执行动画
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Icon(
          Icons.favorite,
          color: Colors.red,
          size: _tweenAnimation.value,
        ),
      ),
    );
  }
}

32.gif

https://book.flutterchina.club/chapter9/intro.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容