本教程向您展示如何在Flutter中构建动画。在介绍动画库中的一些基本概念、类和方法之后,它会引导您完成5个动画示例,向您介绍动画库的不同内容。
基本的动画概念和类
Flutter中的动画系统基于Animation
对象的,widget可以在build
函数中读取Animation
对象的当前值,
并且可以监听动画的状态改变。
Animation<double>
在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation<double>。
Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。
Animation还可以生成除double之外的其他类型值,如:Animation<Color> 或 Animation<Size>。
Animation对象有状态。可以通过访问其value
属性获取动画的当前值。
Animation对象本身和UI渲染没有任何关系。
CurvedAnimation
CurvedAnimation 将动画过程定义为一个非线性曲线.
{% prettify dart %} final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn); {% endprettify %}
CurvedAnimation和AnimationController(在下一节中介绍)都是Animation<double>类型。CurvedAnimation包装它正在修改的对象 - 您不需要子类AnimationController来实现曲线
AnimationController
AnimationController是一个特殊的Animation对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。 例如,下面代码创建一个Animation对象,但不会启动它运行:
{% prettify dart %} final AnimationController controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); {% endprettify %}
AnimationController派生自Animation<double>,因此可以在需要Animation对象的任何地方使用。
但是,AnimationController具有控制动画的其他方法。例如,.forward()
方法可以启动动画。数字的产生与屏幕刷新有关,因此每秒钟通常会产生60个数字,在生成每个数字后,每个Animation对象调用添加的Listener对象。
当创建一个AnimationController时,需要传递一个vsync
参数,存在vsync
时会防止屏幕外动画(译者语:动画的UI不在当前屏幕时)消耗不必要的资源。
通过将SingleTickerProviderStateMixin添加到类定义中,可以将stateful对象作为vsync
的值。你可以在GitHub的animate1中看到这个例子 。
译者语:
vsync
对象会绑定动画的定时器到一个可视的widget,所以当widget不显示时,动画定时器将会暂停,当widget再次显示时,动画定时器重新恢复执行,这样就可以避免动画相关UI不在当前屏幕时消耗资源。 如果要使用自定义的State对象作为vsync
时,请包含TickerProviderStateMixin
。
Tween
默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。例如,以下示例,Tween生成从-200.0到0.0的值:
{% prettify dart %}
final Tween doubleTween = new Tween
Tween是一个无状态(stateless)对象,需要begin
和end
值。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0,但这不是必须的。
Tween继承自Animatable<T>,而不是继承自Animation<T>。Animatable与Animation相似,不是必须输出double值。例如,ColorTween指定两种颜色之间的过渡。
{% prettify dart %} final Tween colorTween = new ColorTween(begin: Colors.transparent, end: Colors.black54); {% endprettify %}
Tween对象不存储任何状态。相反,它提供了evaluate(Animation<double> animation)
方法将映射函数应用于动画当前值。
Animation对象的当前值可以通过value()
方法取到。evaluate
函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。
Tween.animate
要使用Tween对象,请调用其animate()
方法,传入一个控制器对象。例如,以下代码在500毫秒内生成从0到255的整数值。
{% prettify dart %}
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation
注意animate()
返回的是一个Animation,而不是一个Animatable。
以下示例构建了一个控制器、一条曲线和一个Tween:
{% prettify dart %}
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation
动画通知
一个Animation对象可以拥有Listeners和StatusListeners监听器,可以用addListener()
和addStatusListener()
来添加。
只要动画的值发生变化,就会调用监听器。一个Listener最常见的行为是调用setState()来触发UI重建。动画开始、结束、向前移动或向后移动(如AnimationStatus所定义)时会调用StatusListener。
下一节中有一个addListener()
方法的例子。监视动画的进度展示了如何调用addStatusListener()
。
动画示例
本节将向您介绍5个动画示例。每个部分都提供了该示例源代码的链接。
渲染动画
addListener()
和setState()
给widget添加基础的动画。
每次动画生成一个新数字时,监听函数都会调用setState()
。
如何使用必需的vsync
参数定义AnimatedController
了解Dart语言中的 ..
语法。
到目前为止,您已经学会了如何随着时间的推移生成一系列数字,但没有任何东西被渲染到屏幕上。
要使用Animation<>对象进行渲染,请将Animation对象存储为Widget的成员,然后使用其value
值来决定如何绘制。
考虑下面的应用程序,它绘制Flutter logo时没有动画:
{% prettify dart %} import ‘package:flutter/material.dart’;
class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); }
class _LogoAppState extends State
void main() { runApp(new LogoApp()); } {% endprettify %}
修改以上代码,通过一个逐渐放大的动画显示logo。定义AnimationController时,必须传入一个vsync
对象。该vsync
参数在上面AnimationController部分有介绍了 。
高亮部分为修改的代码:
{% prettify dart %} [[highlight]]import ‘package:flutter/animation.dart’;[[/highlight]] import ‘package:flutter/material.dart’;
class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); }
class _LogoAppState extends State
[[highlight]]initState() {[[/highlight]] [[highlight]]super.initState();[[/highlight]] [[highlight]]controller = new AnimationController([[/highlight]] [[highlight]]duration: const Duration(milliseconds: 2000), vsync: this);[[/highlight]] [[highlight]]animation = new Tween(begin: 0.0, end: 300.0).animate(controller)[[/highlight]] [[highlight]]..addListener(() {[[/highlight]] [[highlight]]setState(() {[[/highlight]] [[highlight]]// the state that has changed here is the animation object’s value[[/highlight]] [[highlight]]});[[/highlight]] [[highlight]]});[[/highlight]] [[highlight]]controller.forward();[[/highlight]] [[highlight]]}[[/highlight]]
Widget build(BuildContext context) { return new Center( child: new Container( margin: new EdgeInsets.symmetric(vertical: 10.0), height: [[highlight]]animation.value,[[/highlight]] width: [[highlight]]animation.value,[[/highlight]] child: new FlutterLogo(), ), ); }
[[highlight]]dispose() {[[/highlight]] [[highlight]]controller.dispose();[[/highlight]] [[highlight]]super.dispose();[[/highlight]] } }
void main() { runApp(new LogoApp()); } {% endprettify %}
该addListener()
函数调用了setState()
,所以每次动画生成一个新的数字时,当前帧被标记为脏(dirty),这会导致widget的build()
方法再次被调用。
在build()
中,改变container大小,因为它的高度和宽度现在使用的是animation.value
。动画完成时释放控制器(调用dispose()
方法)以防止内存泄漏。
animate1. 通过简单的修改,您已经在Flutter中创建了第一个动画!这个例子的源代码animate1. 、
用AnimatedWidget简化
addListener()
和setState()
)来给widget添加动画
使用AnimatedWidget创建一个可重用动画的widget。要从widget中分离出动画过渡,请使用AnimatedBuilder。
* Flutter API提供的关于AnimatedWidget的示例包括:AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、FadeTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition。
AnimatedWidget类允许您从setState()
调用中的动画代码中分离出widget代码。AnimatedWidget不需要维护一个State对象来保存动画。
在下面的重构示例中,LogoApp现在继承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在绘制时使用动画的当前值。LogoApp仍然管理着AnimationController和Tween。
{% prettify dart %} // Demonstrate a simple animation with AnimatedWidget
import ‘package:flutter/animation.dart’; import ‘package:flutter/material.dart’;
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation
Widget build(BuildContext context) {
final Animation
class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); }
class _LogoAppState extends State
initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); animation = new Tween(begin: 0.0, end: 300.0).animate(controller); controller.forward(); }
Widget build(BuildContext context) { return new AnimatedLogo(animation: animation); }
dispose() { controller.dispose(); super.dispose(); } }
void main() { runApp(new LogoApp()); } {% endprettify %}
LogoApp将Animation对象传递给基类并用animation.value
设置容器的高度和宽度,因此它的工作原理与之前完全相同。
译者语:和animate1中不同的是,AnimatedWidget(基类)中会自动调用
addListener()
和setState()
。
你可以 在GitHub上找到这个例子的源代码animate2。
监视动画的过程
addStatusListener
来处理动画状态更改的通知,例如启动、停止或反转方向。
当动画完成或返回其开始状态时,通过反转方向来无限循环运行动画
知道动画何时改变状态通常很有用的,如完成、前进或倒退。你可以通过addStatusListener()
来得到这个通知。
以下代码修改animate1示例,以便它监听动态状态更改并打印更新。
下面高亮显示的部分为修改的:
{% prettify dart %}
class _LogoAppState extends State
initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); animation = new Tween(begin: 0.0, end: 300.0).animate(controller) [[highlight]]..addStatusListener((state) => print(“$state”));[[/highlight]] controller.forward(); } //… } {% endprettify %}
运行此代码将输出以下内容:
{% prettify sh %} AnimationStatus.forward AnimationStatus.completed {% endprettify %}
接下来,使用addStatusListener()
在开始或结束时反转动画。这产生了循环效果:
{% prettify dart %}
class _LogoAppState extends State
initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); animation = new 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();
} //… } {% endprettify %}
你可以在GitHub上找到这个例子的源代码animate3。
用AnimatedBuilder重构
build
方法的一部分。如果你只是想用可复用的动画定义一个widget,请使用AnimatedWidget。
Flutter API中AnimatedBuilder的示例包括: BottomSheet、ExpansionTile、
PopupMenu、ProgressIndicator、RefreshIndicator、Scaffold、SnackBar、TabBar、TextField。
animate3示例中的代码存在的一个问题: 更改动画需要更改显示logo的widget。更好的解决方案是将职责分离:
- 显示logo
- 定义Animation对象
- 渲染过渡效果
您可以借助AnimatedBuilder类完成此分离。AnimatedBuilder是渲染树中的一个独立的类。
与AnimatedWidget类似,AnimatedBuilder自动监听来自Animation对象的通知,并根据需要将该控件树标记为脏(dirty),因此不需要手动调用addListener()
。
animate5示例的widget树如下所示:
从widget树的底部开始,渲染logo的代码直接明了:
{% prettify dart %} class LogoWidget extends StatelessWidget { // Leave out the height and width so it fills the animating parent build(BuildContext context) { return new Container( margin: new EdgeInsets.symmetric(vertical: 10.0), child: new FlutterLogo(), ); } } {% endprettify %}
图中的中间三个块都在GrowTransition的build()
方法中创建。GrowTransition本身是无状态的,并拥有定义过渡动画所需的最终变量集合。
build()
函数创建并返回AnimatedBuilder,它将(匿名构建器)方法和LogoWidget对象作为参数。渲染转换的工作实际上发生在(匿名构建器)方法中,
该方法创建一个适当大小的Container来强制缩放LogoWidget。
下面的代码中有一个的问迷惑的题是,child
看起来像被指定了两次。但实际发生的事情是,将外部引用child传递给AnimatedBuilder,AnimatedBuilder将其传递给匿名构造器,
然后将该对象用作其子对象。最终的结果是AnimatedBuilder插入到渲染树中的两个widget之间。
{% prettify dart %} class GrowTransition extends StatelessWidget { GrowTransition({this.child, this.animation});
final Widget child;
final Animation
Widget build(BuildContext context) { return new Center( child: new AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget child) { return new Container( height: animation.value, width: animation.value, child: child); }, child: child), ); } } {% endprettify %}
最后,初始化动画的代码与第一个示例animate1.非常相似。
initState()
方法创建一个AnimationController和一个Tween,然后通过animate()
绑定它们。魔术发生在build()
方法中,该方法返回一个带有LogoWidget作为子对象的GrowTransition对象,以及一个用于驱动过渡的动画对象。
{% prettify dart %} class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); }
class _LogoAppState extends State
initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); final CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn); animation = new Tween(begin: 0.0, end: 300.0).animate(curve); controller.forward(); }
Widget build(BuildContext context) { return new GrowTransition(child: new LogoWidget(), animation: animation); }
dispose() { controller.dispose(); super.dispose(); } }
void main() { runApp(new LogoApp()); } {% endprettify %}
你可以 在GitHub上找到这个例子的源代码animate4。
并行动画
在本节中,您将基于监视动画过程](#monitoring中的示例进行构建, 该示例使用AnimatedWidget循环的进行放大和缩小,现在考虑一下如何再添加一个在透明和不透明之间循环的动画。
每一个Tween管理动画的一种效果。例如:
{% prettify dart %}
final AnimationController controller =
new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
final Animation
你可以通过sizeAnimation.value
来获取大小,通过opacityAnimation.value
来获取不透明度,但AnimatedWidget的构造函数只接受一个动画对象。
为了解决这个问题,该示例创建了自己的Tween对象并显式计算了这些值。
其build
方法.evaluate()
在父级的动画对象上调用Tween函数以计算所需的size
和opacity
值。
下面高亮部分即修改的代码:
{% prettify dart %} import ‘package:flutter/animation.dart’; import ‘package:flutter/material.dart’;
class AnimatedLogo extends AnimatedWidget {
// The Tweens are static because they don’t change.
[[highlight]]static final _opacityTween = new Tween
AnimatedLogo({Key key, Animation
Widget build(BuildContext context) {
final Animation
class LogoApp extends StatefulWidget { _LogoAppState createState() => new _LogoAppState(); }
class _LogoAppState extends State
initState() { super.initState(); controller = new AnimationController( duration: const Duration(milliseconds: 2000), vsync: this); animation = [[highlight]]new CurvedAnimation(parent: controller, curve: Curves.easeIn);[[/highlight]]
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
controller.forward();
}
Widget build(BuildContext context) { return new AnimatedLogo(animation: animation); }
dispose() { controller.dispose(); super.dispose(); } }
void main() { runApp(new LogoApp()); } {% endprettify %}
你可以在GitHub上找到这个例子的源代码animate5。
下一步
本教程为您展示了如何使用Tweens在Flutter中创建动画的基础,但还有很多其他类需要探索。您可以研究一下特定的Tween类、Material Design中特定的动画、ReverseAnimation、共享元素过渡(也称为hero动画)、物理模拟和fling()
方法的动画 。查看动画首页获取最新的可用文档和示例。