这是我参与更文挑战的第11天,活动详情查看: 更文挑战
Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件,同时也是当下最流行的跨端解决方案。
前言
精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。Flutter的动画支持可以轻松实现各种动画类型。许多widget,特别是Material Design widgets, 都带有在其设计规范中定义的标准动画效果,但也可以自定义这些效果。
一、动画的重要性
精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。Flutter的动画支持可以轻松实现各种动画类型。许多widget,特别是Material Design widgets, 都带有在其设计规范中定义的标准动画效果,但也可以自定义这些效果。
动画在各个平台的实现原理都基本相同,是在一段时间内一系列连续变化画面的帧构成的。在 Flutter 中,动画的过程又被量化成一段值区间,我们可以利用这些值设置控件的各个属性来实现动画,其内部由四个关键的部分来实现这一过程。
1.1 插值器(Tweens)
tweens 可为动画提供起始值和结束值。默认情况下,Flutter 中的动画将任何给定时刻的值映射到介于 0.0 和 1.0 之间的 double 值。我们可以使用以下 Tween 将其间值的范围定义为从 -200.0变为 0.0:
tween = Tween<double>(begin: -200, end: 0);
复制代码
我们也可以将值设置为相应需要改变的对象值,比如将起始值设置为红色,结束值设置为蓝色,那么 tweens 产生的动画便是由红渐渐的变成蓝色。如下:
colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
复制代码
1.2 动画曲线(Animation Curves)
Curves 用来调整动画过程中随时间的变化率,默认情况下,动画以均匀的线性模型变化。读者可以通过自定义继承 Curves 的类来定义动画的变化率,比如设置为加速、减速或者先加速后减速等曲线模型。Flutter 内部也提供了一系列实现相应变化率的 Curves 对象:
-
linear
-
decelerate
-
ease
-
easeIn
-
easeOut
-
easeInOut
-
fastOutSlowIn
-
bounceIn
-
bounceOut
-
bounceInOut
-
elasticIn
-
elasticOut
-
elasticInOut
1.3 Ticker providers
Flutter 中的动画以屏幕频繁的重绘而实现,即每秒 60 帧。Ticker 可以被应用在 Flutter 每个对象中,当对象实现了 Ticker 的功能后,每次动画帧改变便会通知该对象。这里,开发者们不需要为对象手动实现 Ticker,flutter 提供了 TickerProvider 类可以帮助我们快速实现该功能。例如,在有状态控件下使用动画时,通常需要在 State 对象下混入 TickerProviderStateMixin。
class _MyAnimationState extends State<MyAnimation>
with TickerProviderStateMixin {
}
复制代码
1.4 动画控制器(AnimationController)
Flutter 中动画的实现还有一个非常重要的类 AnimationController,即动画控制器。很明显,我们用它来控制动画,即动画的启动、暂停等。其接受两个参数,第一个是 vsync,为 Ticker 对象,其作用是当接受到来自 tweens 和 curves 的新值后通知对应对象,第二个 duration 参数为动画持续的时长。
// 混入 SingleTickerProviderStateMixin 使对象实现 Ticker 功能
class _AnimatedContainerState extends State<AnimatedContainer>
with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
// 创建 AnimationController 动画
_controller = AnimationController(
// 传入 Ticker 对象
vsync: this,
// 传入 动画持续时间
duration: new Duration(milliseconds: 1000),
);
startAnimation();
}
Future<void> startAnimation() async {
// 调用 AnimationController 的 forward 方法启动动画
await _controller.forward();
}
@override
Widget build(BuildContext context) {
return Container(
width: _controller.value;
child: //...
);
}
}
复制代码
AnimationController 继承自 Animation,具有一系列控制动画的方法,如可用 forward() 方法来启动动画,可用 repeat() 方法使动画重复执行,也可以通过其 value 属性得到当前值。
二、动画类型
在Flutter中动画分为两类:基于tween或基于p2物理的。
2.1 补间(Tween)动画
在补间动画中,定义了开始点和结束点、时间线以及定义转换时间和速度的曲线。然后由框架计算如何从开始点过渡到结束点。
2.2 基于物理的动画
在基于物理的动画中,运动被模拟为与真实世界的行为相似。例如,当你掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。类似地,将连接在弹簧上的球落下(并弹起)与连接到绳子上的球放下的方式也是不同。
三、Flutter动画库
在为widget添加动画之前,先让我们认识下动画的几个朋友:
Animation:是Flutter动画库中的一个核心类,它生成指导动画的值;
CurvedAnimation:Animation的一个子类,将过程抽象为一个非线性曲线;
AnimationController:Animation的一个子类,用来管理Animation;
Tween:在正在执行动画的对象所使用的数据范围之间生成值。例如,Tween可生成从红到蓝之间的色值,或者从0到255;
3.1 Animation
在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation。
Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。
-
Animation对象有状态。可以通过访问其value属性获取动画的当前值;
-
Animation对象本身和UI渲染没有任何关系;
3.2 CurvedAnimation
CurvedAnimation将动画过程定义为一个非线性曲线。
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves 类定义了许多常用的曲线,也可以创建自己的,例如:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
复制代码
3.3 AnimationController
AnimationController是一个特殊的Animation对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。例如,下面代码创建一个Animation对象:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
复制代码
AnimationController派生自Animation,因此可以在需要Animation对象的任何地方使用。但是,AnimationController具有控制动画的其他方法:
-
forward():启动动画;
-
reverse({double from}):倒放动画;
-
reset():重置动画,将其设置到动画的开始位置;
-
stop({ bool canceled = true }):停止动画;
当创建一个AnimationController时,需要传递一个vsync参数,存在vsync时会防止屏幕外动画消耗不必要的资源,可以将stateful对象作为vsync的值。
在某些情况下,值(position,值动画的当前值)可能会超出AnimationController的0.0-1.0的范围。例如,fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)。位置(position)可以是任何东西,因此可以在0.0到1.0范围之外。CurvedAnimation生成的值也可以超出0.0到1.0的范围。根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。
3.4 Tween
默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。例如,以下示例,Tween生成从-200.0到0.0的值:
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
复制代码
Tween是一个无状态(stateless)对象,需要begin和end值。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0,但这不是必须的。
Tween继承自Animatable,而不是继承自Animation。Animatable与Animation相似,不是必须输出double值。例如,ColorTween指定两种颜色之间的过渡。
final Tween colorTween =
new ColorTween(begin: Colors.transparent, end: Colors.black54);
复制代码
Tween对象不存储任何状态。相反,它提供了evaluate(Animationanimation)方法将映射函数应用于动画当前值。Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。
要使用Tween对象,可调用它的animate()方法,传入一个控制器对象。例如,以下代码在500毫秒内生成从0到255的整数值。
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
// 注意animate()返回的是一个Animation,而不是一个Animatable。
复制代码
以下示例构建了一个控制器、一条曲线和一个Tween:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
复制代码
四、为widget添加动画
在下面的实例中我们为一个logo添加了一个从小放大的动画:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
AnimationStatus animationState;
double animationValue;
@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(() {
animationValue = animation.value;
});
// #docregion addListener
})
..addStatusListener((AnimationStatus state) {
setState(() {
animationState = state;
});
});
// #enddocregion addListener
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 50),
child: Column(
children: <Widget>[
GestureDetector(
onTap: () {
controller.reset();
controller.forward();
},
child: Text('Start', textDirection: TextDirection.ltr),
),
Text('State:' + animationState.toString(),
textDirection: TextDirection.ltr),
Text('Value:' + animationValue.toString(),
textDirection: TextDirection.ltr),
Container(
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
],
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
复制代码
五、为动画添加监听器
有时我们需要知道动画执行的进度和状态,在Flutter中我们可以通过Animation的addListener与addStatusListener方法为动画添加监听器:
addListener:动画的值发生变化时被调用;
addStatusListener:动画状态发生变化时被调用;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
// #enddocregion print-state
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
})
// #docregion print-state
..addStatusListener((state) => print('$state'));
..addListener(() {
// #enddocregion addListener
setState(() {
// The state that has changed here is the animation object’s value.
});
// #docregion addListener
});
controller.forward();
}
复制代码
六、AnimatedWidget
我们可以将AnimatedWidget理解为Animation的助手,使用它可以简化我们对动画的使用,在为widget添加动画的学习中我们不难发现,在不使用AnimatedWidget的情况下需要手动调用动画的addListener()并在回调中添加setState才能看到动画效果,AnimatedWidget将为我们简化这一操作。
在下面的重构示例中,LogoApp现在继承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在绘制时使用动画的当前值。LogoApp仍然管理着AnimationController和Tween。
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
height: animation.value,
width: animation.value,
child: new FlutterLogo(),
),
);
}
}
class LogoApp extends StatefulWidget {
_LogoAppState createState() => new _LogoAppState();
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
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();
}
}
复制代码
6.1 AnimatedBuilder
AnimatedBuilder是用于构建动画的通用widget,AnimatedBuilder对于希望将动画作为更大构建函数的一部分包含在内的更复杂的widget时非常有用,其实你可以这样理解:AnimatedBuilder是拆分动画的一个工具类,借助它我们可以将动画和widget进行分离:
在上面的实例中我们的代码存在的一个问题:更改动画需要更改显示logo的widget。更好的解决方案是将职责分离:
-
显示logo
-
定义Animation对象
-
渲染过渡效果
接下来我们就借助AnimatedBuilder]()类来完成此分离。AnimatedBuilder是渲染树中的一个独立的类, 与[AnimatedWidget类似,AnimatedBuilder自动监听来自Animation对象的通知,不需要手动调用addListener()。
我们根据下图的 widget 树来创建我们的代码:
...
// #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
}
复制代码
Flutter动画类的源码
7.1 Animation
Animation是一个抽象的类,主要保存动画的状态和当前值。最常用的Animation类是Animation,T 有很多类型,可以通过Animation中的 value 属性获得当前动画的值。动画的监听:
-
addListener() 每一帧动画执行的监听
-
addStatusListener() 动画状态改变的监听。
AnimationController
- AnimationController 继承Animation 负责控制动画的执行,停止等。
- AnimationController 会在动画的每一帧,就会生成一个新的值。默认情况下,- – AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。
创建 AnimationController,则需要传入一个 vsync 参数。
Tween
-
默认情况下,AnimationController对象的范围从0.0到1.0。
-
如果你需要不同范围或者不同的数据类型,就需要tween来配置动画以生成不同的范围或数据类型的值。
7.2 AnimatedWidget
abstract class AnimatedWidget extends StatefulWidget {
//创建一个widget,当listenable 发生改变时重构
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
//声明一个Listenable ,帧动画监听
final Listenable listenable;
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
_AnimatedState createState() => _AnimatedState();
...
}
复制代码
AnimatedWidget继承自StatefulWidget,拥有自己的状态,并且实例一个listenable用来监听帧动画,当动画方法变化时,刷新AnimatedWidget。
// __AnimatedState方法源码如下:
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
//添加监听的回调
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
//当帧动画发生改变时触发刷新
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
//调用build()方法,重构AnimatedWidget
@override
Widget build(BuildContext context) => widget.build(context);
}
复制代码
当listenable触发刷新的时候,调用 setState重构AnimatedWidget,虽然到最后还是调用setState,但是刷新的对象是不同的。
7.3 CurvedAnimation
Tween动画默认为我们提供了区间内线性变化,如果我们需要曲线变化,则需要配合使用CurvedAnimation。
class HomePageState extends State<HomePage> with TickerProviderStateMixin{
Animation animation;
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
..addListener((){
setState(() {
});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
alignment: Alignment.topCenter,
child: Container(
margin: EdgeInsets.only(top: animation.value),
width: 100,
height: 100,
child: FlutterLogo(),
),
);
}
}
// CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut); 这里我们使用的是bounceOut效果
class _BounceOutCurve extends Curve {
const _BounceOutCurve._();
@override
double transformInternal(double t) {
return _bounce(t);
}
}
double _bounce(double t) {
if (t < 1.0 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
// 可以看到bounceOut 是继承 Curve类,实现它的transformInternal方法,在transformInternal实现它的轨迹。
复制代码
7.4 使用AnimatedBuilder来重构我们的widget
上面这个demo代码主要是实现一个颜色变化的例子,假如我们现在需要实现一个大小变化的widget呢?是不是要在声明一个SizeAnimationWidget继承AnimationWidget ?显然这样做是可以的。当然我们也有更好的做法,就是使用AnimatedBuilder来重构我们的widget。
AnimatedBuilder继承自抽象的AnimationWidget ,目的为了构建通用的AnimationWidget 实现类,不用每次使用AnimationWidget 都要创建一个实现类。
// AnimatedBuilder源码
class AnimatedBuilder extends AnimatedWidget {
/// Creates an animated builder.
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
// 使用的时候我们只需要传入animation和builder就行了
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
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();
}
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> 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),
);
}
}
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(),
);
}
}
复制代码
总结
通过上面的栗子,我们已经可以方便地封装出一系列控件动画了,但是这种实现方式均需要我们自己提供 Animation 对象,然后通过提供的接口方法来启动我们的动画,控件的属性由 Animation 对象提供并在动画过程中改变而达到动画的效果。为了使动画更加方便,Flutter 帮助了开发者从另一个角度以更简单的方式实现了动画效果——隐式动画组件(ImplicitlyAnimatedWidget)。
通过隐式动画组件,不需要手动实现插值器、曲线等对象,开发者甚至也不需要使用 AnimationController 来启动动画,它的实现方式更贴近对组件本身的操作,我们可以直接通过 setState() 的方法改变隐式动画组件的属性值,其内部自行为我们实现动画过程的过渡效果,即隐藏了所有动画实现的细节