酷炫的动画库——Lottie 源码解析 第一章
简介:Lottie是可以将AE做出的动画,通过
Bodymovin
输出为一个json文件,然后Lottie可以将这个json文件渲染为本地动画,包括Android、iOS、React native、还包括PC端也可以使用。使用LottieView,我们就可以很方便的实现一些复杂的动画,而且也不需要各端自己实现,或者说出现各端实现出来之后效果不一样的情况。如果使用LottieView的话,我们仅仅需要设计师输出一份json文件就可以了。
首先看一下LottieView官方网站上的一些动画的效果:
可以看出这些动画都有很酷炫的效果,如果要通过代码去实现的话就会非常的复杂,但是如果使用Lottie去加载,则非常简单,只需要几行代码就可以搞定,而且帧率大多都会稳定在60FPS。
基本使用
使用方式很简单,只需要在xml文件中声明一个LottieAnimationView
( 当然它还有一些属性可以在xml中设置,这里不再赘述,属性名称都能看的出来其作用),然后在代码中设置:
1 | LottieCompositionFactory.fromAsset(context, "assertName").addListener{ |
只需要上面这样几行代码,就可以实现复杂的动画了。可以看出首先是去assert中加载了一个json动画的资源,加载成功后会调用Listener,给LottieView设置Composition
,然后调用playAnimation()
方法播放就可以了,至于Composition
是什么,我们后面会说到。
源码解析
我们从 lottieView.playAnimation()
开始,因为它是开始播放的方法。那么跟踪源码就能看到:
1 | /** |
可以看到,在 playAnimation()
中,首先判断了 isShown
,也就是当前的LottieView是否显示,然后会调用 lottieDrawable.playAnimation();
那么 lottieDrawable
是什么呢?因为LottieView都是继承自ImageView,所以我们需要给其指定一个资源,那么这个 lottieDrawable 就是要指定的资源,而LottieAnimationView实际上只是起到了一个容器的作用。所以,Lottie动画实现的关键实际上是在这个LottieDrawable中,这个之后再看。
再看第二句,enableOrDisableHardwareLayer()
,该方法是做了是否开启硬件的判断,主要是根据动画解析后的一些规则,例如是mask和matte是否超过4个,以及系统的版本。这是因为在一些场景下,开启硬件加速的表现实际上并不如软件渲染,因此只有当满足一些条件后才会开启。(当然也可以自己设定)
接下来看一下lottieDrawable.playAnimation()
这个方法做了什么:
1 |
|
上面的代码首先是判断了 compositionLayer
是否为空,如果为空的话,就将playAnimation()放进一个延迟执行的任务队列中,然后会调用 animator.playAnimation();
关于compositionLayer是什么,我们先带着这个疑问看下去。这里又调用到了LottieValueAnimator的playAnimation:
1 | public void playAnimation() { |
可以看出,这个方法中调用了三个重要的方法:notifyStart(isReversed())、 setFrame()、和postFrameCallback().
其中notifyStart主要是开始动画,然后看一下setFrame都做了什么:
1 | public void setFrame(float frame) { |
在notifyUpdate中,遍历了当前Lottie的所有动画,并且调用了onAnmationUpdate()方法,这个方法的实现,是在LottieDrawable中:
1 | private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() { |
最终是调用了compositionLayer的setProgress方法:
1 | public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
在这个方法中,计算了当前的progress,然后计算了当前的duration,然后获取了所有的layers,调用了每一个layer的setProgress方法,跟进去之后发现,是调用的BaseLayer的setProgress方法:
1 | void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
又调用了animations的setProgress,
1 | public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { |
这里比较关键, 首先根据之前的progress获取到了上一帧,也就是keyFrame,keyFrame里面存储了一些坐标信息,以及一些坐标点,通过对它的值的改变,达到动画中,View的变化:
1 | public class Keyframe<T> { |
获取了上一个keyFrame 之后,会更新progress,根据新的progress获取到新的一帧,然后当两帧不相等的时候,调用notifyListeners:
1 | public void notifyListeners() { |
调用了onValueChanged,在动画的每一个图层都有实现,最终会调用到lottieDrawable.invalidateSelf()。 这里先强调一个图层的概念,之后回说到。然后这个invalidateSelf() 会调用 LottieAnimationView的invalidateDrawable()方法:
1 | public void invalidateDrawable(@NonNull Drawable dr) { |
这里调用了父类ImageView的invalidateDrawable方法,刷新了ImageView的内容。
以上就是LottieAnimationView的playAnimation的整个过程。
下面给出一个整体的时序图,序号表明了调用顺序:
以上就是LottieAnimationView的playAnimation的整个过程。那么会有一个问题,动画是哪里来的,Lottie如何完成从一个json文件到实现动画的?
实际上,关键就在于 lottieView.setComposition(composition)
这一句代码上,参数composition是解析json动画文件后返回的,在lottieAnimationView.setComposition() 中,又调用了 lottieDrawable.setComposition(),看一下这个方法的实现:
1 | public boolean setComposition(LottieComposition composition) { |
这段代码中有两个关键的部分,一个是buildCompositionLayer() 方法, 还有一个可以看一下while循环内部,我们前面在看playAnimation的源码的时候,当composition为null时,会把当前要执行的动作添加进一个lazyCompositionTask中,它就是在这里执行的,因为这时候已经给composition赋值了。然后看一下buildCompositionLayer()的源码:
1 | private void buildCompositionLayer() { |
可以看到这里初始化了一个CompositionLayer,用的是composition这个对象,先了解一下CompositionLayer是什么:实际上CompositionLayer就是一个布局的图层,它内部有一个drawLayer方法:
1 | void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
从这个方法可以看出,它是包含了一个layer(图层)的集合layers,然后调用了每一个layer的draw方法。从这里我们就能初步发现,实际上Lottie的动画就是由一个一个的layer组成的,通过一层一层的layer,最终实现了Lottie动画,我们这里先给出一个总结,在下一章会详细分析动画的组成,以及动画json文件的分析。