本文章翻译自👉Flutter 官方教程,看完本文章大家就会使用前几篇文章介绍的关键类,就会知道什么时候使用 AnimatedWidget
,什么时候使用 AnimatedBuilder
了。
以下是文章正文
本教程向开发者介绍了,如何在 Flutter 中开发显式动画。会写介绍动画仓库中一些关键的概念、类和方法,使用这些关键的点开发 5 个动画示例。 这些动画相互构建穿插,比较全面的展示动画库。
Flutter SDK 也内置了一些显示动画,比如 FadeTransition
、SizeTransition
和SlideTransition
。这些简单的动画只需要设置开始和结束点就可以触发,比自定义的显示动画容易的多。自定义的
本教程向您展示了如何在Flutter中构建显式动画。在介绍动画库中的一些基本概念、类和方法之后,它会带你走过5个动画示例。这些示例相互构建,向您介绍动画库的不同方面。本篇文章这些动画都会涉及到。
关键概念和关键类
Animation
,Flutter 动画库的核心类,提供动画的值Animation
对象知道动画的状态,但是不知道动画在屏幕上显示的 UIAnimationController
管理Animation
CurvedAnimation
定义一个非线性的动画过程Tween
提供动画的补间值或者效果值,比如补间可以定义为从红色到蓝色等等- 使用
Listener
和StatusListener
来监听动画状态的变化
Widget 使用动画的方式一般有两种,一种是在 build 方法中直接使用动画的值和状态值,一种是将动画传递给其他的 Widget,其他的 Widget 使用传递的动画实现更复杂的效果。
Animation<double>
在 Flutter 中,Animation
对象不知道啥显示到了屏幕上,它仅仅是一个抽象类,只定义了当前的动画值和动画状态。最常用的动画类型是 Animation<double>
。
Animation
对象会时间范围内序列地生成插值,这个插值在定义的上下界之间,Animation
对象的生成的插值,可能是线性的,可能是曲线的,可能是跳跃的,也可以是开发者自定义的映射。只要我们控制了 Animation
对象,就可以实现反转,甚至可以在中间切换动画的方向。
动画也可以是指定的类型,比如颜色动画 Animation<Color>
或者 Animation<Size>
Animation
对象是有状态的,.value
成员变量可以获取动画当前的值,
Animation
对渲染和 build()
方法无感知。
CurvedAnimation
CurvedAnimation
定义了使用非线性的曲线定义了动画的过程。
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
注意
Curves
定义了许多常用的动画曲线,开发者也可以像下面一样自定义:
import 'dart:math'; class ShakeCurve extends Curve { @override double transform(double t) => sin(t * pi * 2); }
大家可以在
Curves
文档中浏览 Flutter 内置的曲线效果。
CurvedAnimation
和 AnimationController
都是 Animation<double>
的子类, 所以它们可以相互传递。那么区别是啥呢? CurvedAnimation
就是修改了原来的动画值实现了曲线效果。但是 AnimationController
不需要泛化来实现曲线。
AnimationController
By default, an AnimationController
linearly produces the numbers from 0.0 to 1.0 during a given duration. For example, this code creates an Animation
object, but does not start it running:
AnimationController
是一个特殊的 Animation
对象,它把动画值的产生与帧关联起来了,只要产生新的帧就产生一个动画值。默认的情况下,AnimationController
会在动画的时间范围内,线性的在 0.0 到 1.0 之间产生值。比如,下面的代码会构造一个 Animation
对象:
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
从类结构来说,AnimationController
是 Animation<double>
的泛化,因此,只要是需要 Animation
对象的地方,都可以传一个 AnimationController
对象。和Animation<double>
相比,AnimationController
提供了动画控制的方法,比如可以调用 .forward()
来开始一个动画。
上面提到了,AnimationController
将值的产生和屏幕刷新关联起来了,所以一般情况下,每秒生成60个数字。只要产生新的值,那么就会调用 Listener
监听者。如果想在帧产生的时候,创建自定义的展示列表可以参考一下 RepaintBoundary
,这个组件定义了绘制的边界。
创建 AnimationController
对象的时候,需要传一个 vsync
参数,vsync
的作用是熄屏时减少动画的损耗。在 StatefulWidget 的 State 中使用动画的话,可以混入 SingleTickerProviderStateMixin
。可以参考 👉animate1。
Tween
一般来说,AnimationController
的变化范围是从 0.0 到 1.0。如果想要不同的变化范围,不同的数据类型,那么可以使用 Tween
,Tween
定义了动画的插值范围或者插值类型。比如下面定义了 -200.0 到 0.0:
tween = Tween<double>(begin: -200, end: 0);
Tween
是无状态的,仅仅接受 begin
和 end
,它完成输入到输出的映射。输入一般来说是 0.0 到 1.0,但是不固定。
Tween
继承自 Animatable<T>
,不是继承自 Animation<T>
。Animatable
不必输出 double 类型的值。比如 ColorTween
指定了两个颜色之间的变化过程。
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
对象不会存储任何状态,它定义了 evaluate(Animation<double> animation)
方法,该方法呢把当前动画的值应用到映射函数上。 Animation
的 .value
可以取到当前动画的值,evaluate 方法还会执行一些常规的操作,比如 0.0 的时候返回 begin , 1.0 的时候返回 end。
Tween.animate
想要使用 Tween
过程的话,就需要调用 Tween
的 animate()
方法,这个方法需要一个 controller 对象。比如下面的代码的含义:在 500 ms 内,生成一系列 0 到 255 内的 int 值。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
注意:
animate()
方法返回的是Animation
对象,不是Animatable
对象。
下面的代码结合了: 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);
动画通知
可以调用 addListener()
和 addStatusListener()
来监听值的变化和状态的变化。
addListener()
内通常会调用 setState()
来重新构建。addStatusListener()
通常是关心动画的开始、结束、移动、反转等状态的变化。
动画例子
下面是五个动画的例子
渲染动画
- 如何使用
addListener()
和setState()
来给一个普通的Widget加上动画- 每产生一个动画值,都在
addListener()
方法中调用setState()
- 如何定义一个带有
vsync
必须参数的AnimationController
- 通过 “
..addListener
” 理解 “..
” 关键字,- 使用
_
开头来让类私有
目前我们已经知道如何产生动画需要的值,但是呢还没有将值与显示绑定出来。想要使用 Animation
对象渲染的话,需要把 Animation
对象作为 我们 Widget 的成员变量,然后决定怎么使用动画的值进行绘制。
下面是不带动画效果的 Flutter 的 Logo
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);
@override
_LogoAppState 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
。 vsync
的构造需要借助 SingleTickerProviderStateMixin
第二步:使用 Tween<double>
将 0-1 转为 0-300
第三步:添加动画的监听者,addListener
方法体中调用 setState
,持续的重续构建
第四步:把原来指定的宽高变为动画的宽高
第五步:在 dispose
中释放controller,避免内存泄漏
代码如下:
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);
// #docregion addListener
//第二步
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
// #enddocregion addListener
//第三步
setState(() {
// The state that has changed here is the animation object’s value.
});
// #docregion addListener
});
// #enddocregion addListener
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(),
),
);
}
@override
void dispose() {
//第五步
controller.dispose();
super.dispose();
}
}
效果如下:
代码中声明动画的地方用到了 ..
,表示返回值的基础上调用方法。
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
// ···
});
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
// ···
});
这两段代码含义是相同的,都是声明动画对象,并且添加监听者
使用 AnimatedWidget 简化
- 如何使用
AnimatedWidget
代替addListener
+setState
的方式来创建动画- 使用
AnimatedWidget
创建一个可复用动画的 Widget。AnimatedBuilder
可以实现 Widget 与 动画分离。
The AnimatedWidget
base class allows you to separate out the core widget code from the animation code. AnimatedWidget
doesn’t need to maintain a State
object to hold the animation. Add the following AnimatedLogo
class:
AnimatedWidget
基类可以将 核心Widget 从动画代码中分离出来。AnimatedWidget
不再需要维护添加动画的 State对象
。如下所示:
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
// #docregion AnimatedLogo
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({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: Container(
margin: const EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: const FlutterLogo(),
),
);
}
}
// #enddocregion AnimatedLogo
class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);
@override
_LogoAppState 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 = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
和第一个代码相比, AnimatedLogo是专门创建执行动画的 Widget,LogoApp 是生成动画对象的地方。这样两者就分离了。实现的效果是一样的。
监听动画过程
一般来说,开发者经常需要知道动画状态的改变,比如完成、向前、反转等等。我们可以通过 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((state) => print('$state'));
controller.forward();
}
// ...
}
看第一处的代码,我们增加了状态的监听,并且打印了状态。 控制台会如下打印:
AnimationStatus.forward
AnimationStatus.completed
我们可以通过状态的监听来实现无限循环的效果。在到 1 的时候,反向动画。在到 0 的时候正向动画。
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();
}
// ...
}
使用 AnimatedBuilder 重构
我们前一个LogoApp 和 AnimatedLogo 配合的代码有一点问题,我们每次动画会调用_LogoAppState 的 build 方法,然后就会构建新的 AnimatedLogo,AnimatedLogo 就会构建,然后每次都会构建新的 FlutterLogo,其实 FlutterLogo 是不用构建的,只要构建它上层的 Container,修改大小即可。所以问题就是 每次多构建了 FlutterLogo。 一个解决此问题的办法是:把功能分离到不同的类:
- 渲染 logo
- 定义
Animation
对象 - 渲染动画
可以使用 AnimatedBuilder
来实现分离的效果。在渲染树上,AnimatedBuilder
是一个单独的类。和 AnimatedWidget
一样, AnimatedBuilder
会自动监听 Animation
对象发出的通知,比如值的变化、状态的变化,并且在动画值变化的时候自动将 Widget 树标记为脏的,所以开发者不需要自己调用 addListener()
方法。
代码如下:
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());
// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
const LogoWidget({Key? key}) : super(key: key);
// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
// #enddocregion LogoWidget
// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
const GrowTransition({required this.child, required this.animation, Key? key})
: super(key: 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,
),
);
}
}
// #enddocregion GrowTransition
class LogoApp extends StatefulWidget {
const LogoApp({Key? key}) : super(key: key);
@override
_LogoAppState createState() => _LogoAppState();
}
// #docregion print-state
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();
}
// #enddocregion print-state
@override
Widget build(BuildContext context) {
return GrowTransition(
child: const LogoWidget(),
animation: animation,
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
// #docregion print-state
}
上面代码形成的 Widget 树是这样的:
从 Widget 树的底部开始看,LogoWidget 是渲染 logo 的组件,很简单:
class LogoWidget extends StatelessWidget {
const LogoWidget({Key? key}) : super(key: key);
// Leave out the height and width so it fills the animating parent
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 10),
child: const FlutterLogo(),
);
}
}
LogoWidget 移除了宽高的设置,就是填充满父布局,至于父布局是多少它不关心。
中间的三块都是在 GrowTransition 的 build()
方法中构建的,下面的代码会展示出来。
GrowTransition
本身是无状态的并且持有 final 的定义动画的 animation
属性。
build() 方法构造了 AnimatedBuilder
,这个 AnimatedBuilder
把匿名方法和 LogoWidget
对象作为入参。渲染和动画就在匿名方法中,我们看到匿名方法的 SizedBox 的宽高是动画的值,就是强制 child(LogoWidget)是动画的宽高,达到了动画缩放的效果。
class GrowTransition extends StatelessWidget {
const GrowTransition({required this.child, required this.animation, Key? key})
: super(key: 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,
),
);
}
}
把这些组合起来的代码,有点像最开始的动画代码。在 initState()
中创建 AnimationController
和 Tween
,使用 animate()
把他们关联起来。
是不是很神奇动画就这样实现了,build()
方法返回了 GrowTransition
,GrowTransition
需要一个字节点 LogoWidget
,和一个动画对象,这就是上面列出来的。
同时动画
下面,我们在前面讲到的利用动画状态实现循环的基础上,实现同时动画的效果。AnimatedWidget
实现了从无到有的动画过程。想象一下,我们在从无到有的基础上,再加上透明度的变化。
例子主要展示了在一个 controller 上使用多个 tween,每一个 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
可以得到尺寸动画的值,opacityAnimation.value
可以得到透明度动画的值。但是呢 AnimatedWidget
仅仅需要一个 Animation
对象。为了解决这个问题,例子中给的方式是 AnimatedLogo 创建自己的 Tween
对象,并且显示的计算值。
具体的代码是这样的,AnimatedLogo
封装了自己的 Tween
对象,并且在 build 方法中调用 Tween.evaluate()
犯法,evaluate 计算的依据是父动画的值。计算的结果是尺寸和透明度的值。
具体的代码是这样的:
class AnimatedLogo extends AnimatedWidget {
const AnimatedLogo({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);
// Make the Tweens static because they don't change.
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({Key? key}) : super(key: key);
@override
_LogoAppState 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();
}
}
前面的代码没有变,变的只是 AnimatedLogo 。整体效果如下:
总结
本节主要是讲了一些 Flutter 动画的基础,比如 Tweens
、 AnimaitonController
等等。还有很多类开发者可以继续探索。大家如果想看复杂的 Tweens
、Material 设计的动画、Hero 动画等可以看之前的 一文可以让你在Flutter动画上讲两句里面有很多资源。