Flutter动画简介

Flutter中动画抽象

Animation

Animation是一个抽象类,它本身和UI渲染没有任何关系,而它主要的功能是保存动画的插值和状态;

其中一个比较常用的Animation类是Animation<double>

Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。

Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,这由Curve来决定。

根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。

Animation还可以生成除double之外的其他类型值,如:Animation<Color>Animation<Size>。在动画的每一帧中,我们可以通过Animation对象的value属性获取动画的当前状态值。

动画通知

可以通过Animation来监听动画每一帧以及执行状态的变化,Animation有如下两个方法:

  1. addListener();它可以用于给Animation添加帧监听器在每一帧都会被调用。帧监听器中最常见的行为是改变状态后调用setState()来触发UI重建
  2. addStatusListener();它可以给Animation添加“动画状态改变”监听器;动画开始、结束、正向或反向(见AnimationStatus定义)时会调用状态改变的监听器

Curve

动画过程可以是匀速的、匀加速的或者先加速后减速等。

Flutter中通过Curve(曲线)来描述动画过程

我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。

我们可以通过CurvedAnimation来指定动画的曲线,如:

1
2
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);

CurvedAnimationAnimationController都是Animation<double>类型。

CurvedAnimation可以通过包装AnimationControllerCurve生成一个新的动画对象 ,我们正是通过这种方式来将动画和动画执行的曲线关联起来的。

我们指定动画的曲线为Curves.easeIn,它表示动画开始时比较慢,结束时比较快。

Curves (opens new window)类是一个预置的枚举类,定义了许多常用的曲线,下面列几种常用的:

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

除了上面列举的, Curves (opens new window)类中还定义了许多其他的曲线,

当然我们也可以创建自己Curve,例如我们定义一个正弦曲线:

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

AnimationController

AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。

AnimationController会在动画的每一帧,就会生成一个新的值。

默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。

例如,下面代码创建一个Animation对象(但不会启动动画):

1
2
3
4
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);

AnimationController生成数字的区间可以通过lowerBoundupperBound来指定,如:

1
2
3
4
5
6
final AnimationController controller = AnimationController( 
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);

AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用。

但是,AnimationController具有控制动画的其他方法,例如forward()方法可以启动正向动画,reverse()可以启动反向动画。

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

另外在动画的每一帧,Animation对象会调用其帧监听器,等动画状态发生改变时(如动画结束)会调用状态改变监听器。

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

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

Ticker

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

1
2
3
4
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的值

Tween

1)简介

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

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

Tween构造函数需要beginend两个参数。

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

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

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

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

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

Tween.animate

要使用 Tween 对象,需要调用其animate()方法,然后传入一个控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。

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

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

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

1
2
3
4
5
6
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);

线性插值lerp函数

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

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

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

1
2
3
4
5
6
// 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 按照不同映射公式进行映射实现的。

动画基本结构及状态监听

动画基本结构

在Flutter中我们可以通过多种方式来实现动画,下面通过一个图片逐渐放大示例的不同实现来演示Flutter中动画的不同实现方式的区别。

基础版本

下面我们演示一下最基础的动画实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute({Key? key}) : super(key: key);

@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}

//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

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

//匀速
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() => {});
});

//启动动画(正向执行)
controller.forward();
}

@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}

@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}

上面代码中addListener()函数调用了setState(),所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()方法再次被调用

而在build()中,改变Image的宽高,因为它的高度和宽度现在使用的是animation.value ,所以就会逐渐放大。

值得注意的是动画完成时要释放控制器(调用dispose()方法)以防止内存泄漏。

上面的例子中并没有指定Curve,所以放大的过程是线性的(匀速),下面我们指定一个Curve,来实现一个类似于弹簧效果的动画过程,我们只需要将initState中的代码改为下面这样即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//使用弹性曲线
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() => {});
});
//启动动画
controller.forward();
}

运行后效果如图9-1所示:

图9-1

使用AnimatedWidget简化

上面示例中通过addListener()setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。

AnimatedWidget类封装了调用setState()的细节,并允许我们将 widget 分离出来,重构后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import 'package:flutter/material.dart';

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

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

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

@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}

class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;

@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2), vsync: this);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}

@override
Widget build(BuildContext context) {
return AnimatedImage(
animation: animation,
);
}

@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}

用AnimatedBuilder重构

用AnimatedWidget 可以从动画中分离出 widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget 中,假设如果我们再添加一个 widget 透明度变化的动画,那么我们需要再实现一个AnimatedWidget,这样不是很优雅,如果我们能把渲染过程也抽象出来,那就会好很多,而AnimatedBuilder正是将渲染逻辑分离出来, 上面的 build 方法中的代码可以改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("imgs/avatar.png"),
builder: (BuildContext ctx, child) {
return Center(
child: SizedBox(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}

上面的代码中有一个迷惑的问题是,child看起来像被指定了两次。

但实际发生的事情是:将外部引用child传递给AnimatedBuilder后,AnimatedBuilder再将其传递给匿名构造器, 然后将该对象用作其子对象。最终的结果是AnimatedBuilder返回的对象插入到 widget 树中。

也许你会说这和我们刚开始的示例差不了多少,其实它会带来三个好处:

  1. 不用显式的去添加帧监听器,然后再调用setState() 了,这个好处和AnimatedWidget是一样的。

  2. 更好的性能:因为动画每一帧需要构建的 widget 的范围缩小了,如果没有buildersetState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。

  3. 通过AnimatedBuilder可以封装常见的过渡效果来复用动画。下面我们通过封装一个GrowTransition来说明,它可以对子widget实现放大动画:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class GrowTransition extends StatelessWidget {
    const GrowTransition({Key? key,
    required this.animation,
    this.child,
    }) : super(key: key);

    final Widget? child;
    final Animation<double> animation;

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

    这样,最初的示例就可以改为:

    1
    2
    3
    4
    5
    6
    7
    ...
    Widget build(BuildContext context) {
    return GrowTransition(
    child: Image.asset("images/avatar.png"),
    animation: animation,
    );
    }

    Flutter中正是通过这种方式封装了很多动画,如:FadeTransition、ScaleTransition、SizeTransition等,很多时候都可以复用这些预置的过渡类。

动画状态监听

上面说过,我们可以通过AnimationaddStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:

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

示例

我们将上面图片放大的示例改为先放大再缩小再放大……这样的循环动画。要实现这种效果,我们只需要监听动画状态的改变即可,即:在动画正向执行结束时反转动画,在动画反向执行结束时再正向执行动画。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
//图片宽高从0变到300
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();
}
});

//启动动画(正向)
controller.forward();
}