基本概念
Animation 对象知道动画目前的状态(例如,是否开始,暂停,前进或倒退),但是对屏幕上显示的内容一无所知。
AnimationController 管理 Animation。
CurvedAnimation 定义动画进度为非线性曲线。
Tween 为动画对象插入一个范围值。例如,Tween 可以定义插入值由红到蓝,或从 0 到 255。
使用 Listeners 和 StatusListeners 监视动画状态变化。
Animation<double>
Animation是一个已知当前值和状态(已完成或已解除)的抽象类,它的当前值使用.value获取。一个 Animation 对象在一段时间内,持续生成介于两个值之间的插入值。这个 Animation 对象输出的可能是直线,曲线,阶梯函数,或者任何自定义的映射。根据 Animation 对象的不同控制方式,它可以反向运行,或者中途切换方向。
CurvedAnimation
CurvedAnimation 和 AnimationController都是 Animation<double> 类型,所以可以互换使用。CurvedAnimation 封装正在修改的对象 — 不需要将 AnimationController 分解成子类来实现曲线。
Tween
在默认情况下,AnimationController 对象的范围是 0.0-0.1。如果需要不同的范围或者不同的数据类型,可以使用 Tween 配置动画来插入不同的范围或数据类型。例如下面的示例中,Tween 的范围是 -200 到 0.0。
tween = Tween<double>(begin: -200, end: 0);
Tween是无状态的,只有begin和end,Tween的作用只是为了定义输入范围(通常是0.0-1.0,但也不一定)到输出范围的映射。
Tween 对象不存储任何状态。而是提供 evaluate(Animation<double> animation) 方法,将映射函数应用于动画当前值。
ColorTween 指定了两种颜色之间的过程:
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
要使用 Tween 对象,请在 Tween 调用 animate(),传入控制器对象。animate() 方法会返回一个 Animation。
例如,下面的代码在 500 ms 的进程中生成 0-255 范围内的整数值 :
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
Tween<T extends dynamic>继承自Animatable<T>,其实现类有:
https://api.flutter-io.cn/flutter/animation/Tween-class.html
AlignmentGeometryTween, AlignmentTween, BorderRadiusTween, BorderTween,BoxConstraintsTween, ColorTween, ConstantTween, DecorationTween,EdgeInsetsGeometryTween,EdgeInsetsTween,FractionalOffsetTween,IntTween,MaterialPointArcTween,Matrix4Tween,RectTween,RelativeRectTween,ReverseTween,ShapeBorderTween,SizeTween,StepTween,TextStyleTween, ThemeDataTween
Animatable 类介绍
通常,输入动画的值通常在0.0到1.0的范围内。 但是,原则上可以提供任何值。
通常,输入动画的值通常在0.0到1.0的范围内。 但是,原则上可以提供任何值。
下面的示例展示了一个控制器AnimationController,一个曲线Curve,和一个 Tween的组合使用:
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);
使用动画
渲染动画
要显示一个 Animation 对象,需将 Animation 对象存储为您的 widget 成员,然后用它的值来决定如何绘制。
比如显示一个Flutter Logo的动画,在使用Animation之前,代码如下:
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: 300,
width: 300,
child: FlutterLogo(),
),
);
}
}
在使用Animation的值来改变Widget的属性,这里是Container的height和width,代码修改后如下:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
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)
..addListener(() {
setState(() {
// The state that has changed here is the animation object’s value.
});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
使用 AnimatedWidget 进行简化
关于AnimatedWidget,在之前的一篇文章中已经做过介绍:
利用 AnimatedWidget 创建一个可以运行重复使用动画的 widget
AnimatedWidget 基本类可以从动画代码中区分出核心 widget 代码。 AnimatedWidget 不需要保持 State 对象来 hold 动画。
AnimatedWidget对于无状态的Widget最有用。 要使用AnimatedWidget,只需将其子类化并实现build函数。
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
在使用AnimatedWidget将动画逻辑封装到自定义的Widget后,相应的调用方式代码修改如下:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
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);
}
@override
Widget build(BuildContext context) => AnimatedLogo(animation: animation);
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
AnimatedBuilder
较好的解决办法是,将任务区分到不同类里:
- 渲染logo Render the logo
- 定义动画对象 Define the Animation object
- 渲染过渡效果 Render the transition
这时候可以将要产生动画的Widget的渲染逻辑单独到一个类中;然后自定义一个GrowTransition类,封装了AnimatedBuilder,
AnimatedBuilder的参数animation传入动画对象(通常是一个AnimationController对象)和一个builder,在这里builder里指定过渡效果的渲染逻辑,第三个参数就是要产生动画效果的Widget对象。
全部代码如下:
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() => runApp(LogoApp());
// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
Widget build(BuildContext context) => Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
// #enddocregion LogoWidget
// #docregion GrowTransition
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) => Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
// #enddocregion GrowTransition
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
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) => GrowTransition(
child: LogoWidget(),
animation: animation,
);
@override
void dispose() {
controller.dispose();
super.dispose();
}
// #docregion print-state
}
AnimatedBuilder VS AnimatedWidget
如果在内置的显式动画FooTransition中找不到合适的Widget,那么可以使用AnimatedBuilder 和继承AnimatedWidget的方式来自定义显式动画,那如何在二者之中选择呢?
通常建议使用继承AnimatedWidget的方式,不过如果如果创建controller的父Widget非常简单,则仅为动画制作单独的独立Widget可能会产生太多额外的代码。 在这种情况下,AnimatedBuilder就是您所需要的。当然上面说到的性能优化点也是可以一起考虑来进行选择的。
使用一个controller和多个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);
全部代码如下:
void main() => runApp(LogoApp());
// #docregion diff
class AnimatedLogo extends AnimatedWidget {
// 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);
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Opacity(
opacity: _opacityTween.evaluate(animation),
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: FlutterLogo(),
),
),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
// #docregion AnimationController, tweens
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
// #enddocregion AnimationController, tweens
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();
}
}
参考视频和博文:
Making your first directional animations with built-in explicit animations
Directional animations with built-in explicit animations
Creating custom explicit animations with AnimatedBuilder and AnimatedWidget