本文章翻译自👉Flutter 官方教程,看完本文章大家就会使用前几篇文章介绍的关键类,就会知道什么时候使用 AnimatedWidget,什么时候使用 AnimatedBuilder 了。
以下是文章正文
本教程向开发者介绍了,如何在 Flutter 中开发显式动画。会写介绍动画仓库中一些关键的概念、类和方法,使用这些关键的点开发 5 个动画示例。 这些动画相互构建穿插,比较全面的展示动画库。
Flutter SDK 也内置了一些显示动画,比如 FadeTransition 、SizeTransition 和SlideTransition 。这些简单的动画只需要设置开始和结束点就可以触发,比自定义的显示动画容易的多。自定义的
本教程向您展示了如何在Flutter中构建显式动画。在介绍动画库中的一些基本概念、类和方法之后,它会带你走过5个动画示例。这些示例相互构建,向您介绍动画库的不同方面。本篇文章这些动画都会涉及到。
关键概念和关键类
Animation,Flutter 动画库的核心类,提供动画的值Animation对象知道动画的状态,但是不知道动画在屏幕上显示的 UIAnimationController管理AnimationCurvedAnimation定义一个非线性的动画过程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动画上讲两句里面有很多资源。