Flutter 动画部分 API 有些凌乱,我看有的人说挺简单的,但是在我来看系统 API 不是很友好,逻辑思路是扭着的,不知道有没有相同感受的朋友,这篇文章中我详细说说,把点都写上,给大家一个全面的参考
动画原理
这部分解释还得是官方文档解释的清晰啊,我直接复制过来了:
在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。很明显,帧率越高则动画就会越流畅!一般情况下,对于人眼来说,动画帧率超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑,而超过32FPS,人眼基本上就感受不到差别了。由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标,而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用能达到的帧率是基本是持平的。
Fluuter 理论刷新率是 120FPS,大家想啊,Flutter 自有线程负责 UI 计算,自然可以用系统 60帧更高的帧数去做,所以这也是 Flutter 在有复杂动画时依然能达到接近 60帧的原理所在,因为实际上是用比 60更高的帧数去做的,自然丢帧就少了,总有新的帧可以显示
动画 API 代码分层:
Animation- 动画 API 的基础,所有的动画最终都是用Animation类型来承载。Animation主要职能是保存动画每一帧的数值Curve- 动画的插值器,用于动画每帧数值的计算的,这个大家都是熟悉AnimationController- 动画的控制器,动画操控,监听部分都写在这里Tween- 数值区间,主要用来处理不同数据类型的数据,比如 widget 动画中最常用的 都 double,color 等Ticker- 负责分发 async,触发页面 rebuild,详细的去看源码研究,代码一般用不上这个
看上图,Flutter 动画 API 中根接口就是2个:Animation、Animatable,Animation 作为承载数据的基本类型自有他自己是 Animation 类型的,其他 API 都是 Animatable 的,Animatable 可以理解位动画中数据的变化,Animatable 通过 animate 这个方法返回一个 Animation 对象来参与后面的计算,基本就是这个套路
官方解释:
主角当然是我们的Animation类了,它可以借助Animatable进行强化 Animatable通过animate函数接收一个Animation对象,再返回Animation对象,这不就是包装吗? 通过Animation对象回调即可获取规律变画的值,进行渲染。这是动画的基本
吐槽下 API 设计:
有人说这个包装的思路,但是我认为这个套路真是糟糕至极
- 第一,上手就不容易理解,容易懵,非常不友好
- 其次从代码的功能分层来看也不合理,
AnimationController作为控制器天然就是最外层 API,其他 API 作为一个个功能,应该.set进AnimationController或是builder里面,而不是每个功能 API.animate再返回Animation类型的对象,而且就连AnimationController也要.animate办顶到别人身上,而不是别人绑定到AnimationController身上,整个 API 全 NM 是反着来的,恶心死了
要是我的话,我会通过
AnimationController
..durntion
..Curve
..Tween
这种方式来设计代码,可以选择放在 AnimationController 构造函数里面,也可以专门搞一个 builder 出来,总之这样逻辑思路是顺应大家平时思路的,写着舒服,看着简单,容易理解,好扩展。Animation依然还是承载具体数据的类,只不过变成功能类了,Animatable 作为增加数据变化的抽象接口使用,屏蔽在内部,不暴露出来。
动画监听方法,就是2个:
addListener()- 每一帧都会被调用一次,一般就是用 setState() 来触发UI重建addStatusListener()- 监听动画状态的变化
动画状态:
dismissed- 动画在起始点停止forward- 动画正在正向执行reverse- 动画正在反向执行completed- 动画在终点停止
Animation
Animation 是抽象基类,是 Flutter 动画 API 这块的基础。Animation 中就是储存动画每一帧的数据,其他 API 使用代理方式(其实说装饰着模式也行,虽然代码看着不像,但是是这个意思)给 Animation 添加功能,然后还返回 Animation 这个根类型对象
Animation 使用泛型来承载不同的数据类型,常用的就是:Animation<Color>、 Animation<Size>、Animation<double> 这3个了
Curve
final CurvedAnimation curve = new CurvedAnimation(
parent: controller,
curve: Curves.easeIn);
Curve 是动画插值器,Flutter 里面包含下面的类型:
lineardecelerateeaseeaseIneaseOuteaseInOutfastOutSlowInbounceInbounceOutbounceInOutelasticInelasticOutelasticInOut
详细的不解释了,大家看下面这篇效果图吧:
自定义 Curve 也是可以的,不过我一直不明白这里的数学知识...
例如我们定义一个正弦曲线:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController
AnimationController 用于控制动画,主要就是3个方法:
forward()- 启动stop()- 停止reverse()- 反向播放
AnimationController 派生自 Animation<double>,每一帧动画的数值可以通过 Animation.value 获取,默认的数值区间是:0.0 到 1.0,想更改数据范围,看下面的设置
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
// 可以通过lowerBound和upperBound 来指定数值区间:
final AnimationController controller = new AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
Tween
AnimationController 默认的数值范围就是[0.0,1.0],即便可以设置也只是可以设置最大值,最小值,若是我们想从数值大到数值小的变化呢,这时候我们就需要可以更自由设置数据的类:Tween 了
Tween 可以自由设置起始值和结束值:
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
final Tween colorTween = new ColorTween(
begin: Colors.transparent,
end: Colors.black54);
Tween 可以计算很多类型的数据,每种类型都有专门对应的子类:
ColorTweenSizeTweenIntTweenRectTweenReverseTweenStepTweenConstantTween
Ticker
Ticker 这部分是官方的解释,我看着还行,一般我们不会用到这个,用也就是个 this 罢了,大家记住这个负责分发 async 信号就行了
当创建一个AnimationController时,需要传递一个vsync参数,它接收一个TickerProvider类型的对象,它的主要职责是创建Ticker,定义如下:
abstract class TickerProvider {
//通过一个回调创建一个Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter应用在启动时都会绑定一个SchedulerBinding,通过SchedulerBinding可以给每一次屏幕刷新添加回调,而Ticker就是通过SchedulerBinding来添加屏幕刷新回调,这样一来,每次屏幕刷新都会调用TickerCallback。使用Ticker(而不是Timer)来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源,因为Flutter中屏幕刷新时会通知到绑定的SchedulerBinding,而Ticker是受SchedulerBinding驱动的,由于锁屏后屏幕会停止刷新,所以Ticker就不会再触发。
通常我们会将SingleTickerProviderStateMixin添加到State的定义中,然后将State对象作为vsync的值,这在后面的例子中可以见到。
页面 widget 需要继承 Ticker 类以实现动画的页面刷新:SingleTickerProviderStateMixin
动画循环执行
目前只找到了添加监听这一种方法
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 1), vsync: this);
//图片宽高从0变到300
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();
}
Flutter 动画的基础套路
1. 构建 AnimationController 对象
2. 构建 Tween 对象,使用 animate 方法绑定 AnimationController
3. widget 的某些属性关联动画数值:animation.value
4. animationController?.forward(); 开始执行动画
5. 页面 widget 继承 SingleTickerProviderStateMixin 类
这个大家看代码就行了,万年不变的套路就是这样:
class TestWidgetState extends State<TestWidget> with SingleTickerProviderStateMixin{
Animation<double> animation;
AnimationController animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
animation = CurvedAnimation(parent: animationController, curve: Curves.bounceInOut);
animation = Tween(begin: 100.0, end: 300.0).animate(animationController)
..addListener(() {
setState(() {});
});
}
dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
margin: EdgeInsets.only(bottom: 20),
width: animation.value,
height: animation.value,
color: Colors.blueAccent,
),
RaisedButton(
child: Text("放大"),
onPressed: () {
animationController?.forward();
},
),
],
),
);
}
}
AnimatedWidget
AnimatedWidget 这块大家看看就行了,一般不直接用他。AnimatedWidget 是一个基类,帮我们处理了 addListener()和setState() 的工作。思路是我们自己写动画 widget 时继承 AnimatedWidget 这个类,在构造方法中传入 animation 动画对象,在布局中绑定动画属性参与的属性值
看看官方文档的代码,熟悉就行,一般我们不这么写
class AnimatedImage extends AnimatedWidget {
AnimatedImage({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return new Center(
child: Image.asset("imgs/avatar.png",
width: animation.value,
height: animation.value
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
@override
_ScaleAnimationRouteState createState() => new _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute1>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//图片宽高从0变到300
animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(animation: animation,);
}
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
AnimatedBuilder
AnimatedBuilder 是对上面 AnimatedWidget 的进一步简化,AnimatedWidget 还要求我们把布局写到继承 AnimatedWidget 的类里面,可是实际上我们都是在页面中书写所有的页面 widget 的,AnimatedBuilder 实现了关于 widget 和 animation 的抽象,widget 和 animation 都变成了传递的数据了,这样我们在构建动画时才能真的实现灵活,随心所欲,书写方便了
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
),
);
}
}