酷炫的动画库——Lottie源码解析(一)

酷炫的动画库——Lottie 源码解析 第一章

简介:Lottie是可以将AE做出的动画,通过Bodymovin 输出为一个json文件,然后Lottie可以将这个json文件渲染为本地动画,包括Android、iOS、React native、还包括PC端也可以使用。使用LottieView,我们就可以很方便的实现一些复杂的动画,而且也不需要各端自己实现,或者说出现各端实现出来之后效果不一样的情况。如果使用LottieView的话,我们仅仅需要设计师输出一份json文件就可以了。

Lottie官方文档

首先看一下LottieView官方网站上的一些动画的效果:

可以看出这些动画都有很酷炫的效果,如果要通过代码去实现的话就会非常的复杂,但是如果使用Lottie去加载,则非常简单,只需要几行代码就可以搞定,而且帧率大多都会稳定在60FPS。

基本使用

使用方式很简单,只需要在xml文件中声明一个LottieAnimationView ( 当然它还有一些属性可以在xml中设置,这里不再赘述,属性名称都能看的出来其作用),然后在代码中设置:

1
2
3
4
5
6
LottieCompositionFactory.fromAsset(context, "assertName").addListener{
lottieView.setComposition(it)
lottieView.playAnimation()
}.addFailureListener{
//Load Error
}

只需要上面这样几行代码,就可以实现复杂的动画了。可以看出首先是去assert中加载了一个json动画的资源,加载成功后会调用Listener,给LottieView设置Composition,然后调用playAnimation() 方法播放就可以了,至于Composition 是什么,我们后面会说到。

源码解析

我们从 lottieView.playAnimation() 开始,因为它是开始播放的方法。那么跟踪源码就能看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Plays the animation from the beginning. If speed is < 0, it will start at the end
* and play towards the beginning
*/
@MainThread
public void playAnimation() {
if (isShown()) {
lottieDrawable.playAnimation();
enableOrDisableHardwareLayer();
} else {
wasAnimatingWhenNotShown = true;
}
}

可以看到,在 playAnimation() 中,首先判断了 isShown ,也就是当前的LottieView是否显示,然后会调用 lottieDrawable.playAnimation(); 那么 lottieDrawable 是什么呢?因为LottieView都是继承自ImageView,所以我们需要给其指定一个资源,那么这个 lottieDrawable 就是要指定的资源,而LottieAnimationView实际上只是起到了一个容器的作用。所以,Lottie动画实现的关键实际上是在这个LottieDrawable中,这个之后再看。

再看第二句,enableOrDisableHardwareLayer() ,该方法是做了是否开启硬件的判断,主要是根据动画解析后的一些规则,例如是mask和matte是否超过4个,以及系统的版本。这是因为在一些场景下,开启硬件加速的表现实际上并不如软件渲染,因此只有当满足一些条件后才会开启。(当然也可以自己设定)

接下来看一下lottieDrawable.playAnimation() 这个方法做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@MainThread
public void playAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override
public void run(LottieComposition composition) {
playAnimation();
}
});
return;
}

if (systemAnimationsEnabled || getRepeatCount() == 0) {
animator.playAnimation();
}
if (!systemAnimationsEnabled) {
setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
}
}

上面的代码首先是判断了 compositionLayer 是否为空,如果为空的话,就将playAnimation()放进一个延迟执行的任务队列中,然后会调用 animator.playAnimation(); 关于compositionLayer是什么,我们先带着这个疑问看下去。这里又调用到了LottieValueAnimator的playAnimation:

1
2
3
4
5
6
7
8
public void playAnimation() {
running = true;
notifyStart(isReversed());
setFrame((int) (isReversed() ? getMaxFrame(·) : getMinFrame()));
lastFrameTimeNs = 0;
repeatCount = 0;
postFrameCallback();
}

可以看出,这个方法中调用了三个重要的方法:notifyStart(isReversed())、 setFrame()、和postFrameCallback(). 其中notifyStart主要是开始动画,然后看一下setFrame都做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 public void setFrame(float frame) {
if (this.frame == frame) {
return;
}
this.frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
lastFrameTimeNs = 0;
notifyUpdate();
}

// notifyUpdate()
void notifyUpdate() {
for (ValueAnimator.AnimatorUpdateListener listener : updateListeners) {
listener.onAnimationUpdate(this);
}
}

在notifyUpdate中,遍历了当前Lottie的所有动画,并且调用了onAnmationUpdate()方法,这个方法的实现,是在LottieDrawable中:

1
2
3
4
5
6
7
8
private final ValueAnimator.AnimatorUpdateListener  progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};

最终是调用了compositionLayer的setProgress方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
if (timeRemapping != null) {
float duration = lottieDrawable.getComposition().getDuration();
long remappedTime = (long) (timeRemapping.getValue() * 1000);
progress = remappedTime / duration;
}
if (layerModel.getTimeStretch() != 0) {
progress /= layerModel.getTimeStretch();
}

progress -= layerModel.getStartProgress();
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}

在这个方法中,计算了当前的progress,然后计算了当前的duration,然后获取了所有的layers,调用了每一个layer的setProgress方法,跟进去之后发现,是调用的BaseLayer的setProgress方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
// Time stretch should not be applied to the layer transform.
transform.setProgress(progress);
if (mask != null) {
for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
mask.getMaskAnimations().get(i).setProgress(progress);
}
}
if (layerModel.getTimeStretch() != 0) {
progress /= layerModel.getTimeStretch();
}
if (matteLayer != null) {
// The matte layer's time stretch is pre-calculated.
float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
matteLayer.setProgress(progress * matteTimeStretch);
}
for (int i = 0; i < animations.size(); i++) {
animations.get(i).setProgress(progress);
}
}

又调用了animations的setProgress,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
if (keyframes.isEmpty()) {
return;
}
// Must use hashCode() since the actual object instance will be returned
// from getValue() below with the new values.
Keyframe<K> previousKeyframe = getCurrentKeyframe();
if (progress < getStartDelayProgress()) {
progress = getStartDelayProgress();
} else if (progress > getEndProgress()) {
progress = getEndProgress();
}

if (progress == this.progress) {
return;
}
this.progress = progress;
// Just trigger a change but don't compute values if there is a value callback.
Keyframe<K> newKeyframe = getCurrentKeyframe();

if (previousKeyframe != newKeyframe || !newKeyframe.isStatic()) {
notifyListeners();
}
}

这里比较关键, 首先根据之前的progress获取到了上一帧,也就是keyFrame,keyFrame里面存储了一些坐标信息,以及一些坐标点,通过对它的值的改变,达到动画中,View的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Keyframe<T> {
private static final float UNSET_FLOAT = -3987645.78543923f;
private static final int UNSET_INT = 784923401;

@Nullable private final LottieComposition composition;
@Nullable public final T startValue;
@Nullable public T endValue;
@Nullable public final Interpolator interpolator;
public final float startFrame;
@Nullable public Float endFrame;

private float startValueFloat = UNSET_FLOAT;
private float endValueFloat = UNSET_FLOAT;

private int startValueInt = UNSET_INT;
private int endValueInt = UNSET_INT;

private float startProgress = Float.MIN_VALUE;
private float endProgress = Float.MIN_VALUE;

// Used by PathKeyframe but it has to be parsed by KeyFrame because we use a JsonReader to
// deserialzie the data so we have to parse everything in order
public PointF pathCp1 = null;
public PointF pathCp2 = null;
}

获取了上一个keyFrame 之后,会更新progress,根据新的progress获取到新的一帧,然后当两帧不相等的时候,调用notifyListeners:

1
2
3
4
5
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}

调用了onValueChanged,在动画的每一个图层都有实现,最终会调用到lottieDrawable.invalidateSelf()。 这里先强调一个图层的概念,之后回说到。然后这个invalidateSelf() 会调用 LottieAnimationView的invalidateDrawable()方法:

1
2
3
4
5
6
7
8
9
10
@Override public void invalidateDrawable(@NonNull Drawable dr) {
if (getDrawable() == lottieDrawable) {
// We always want to invalidate the root drawable so it redraws the whole drawable.
// Eventually it would be great to be able to invalidate just the changed region.
super.invalidateDrawable(lottieDrawable);
} else {
// Otherwise work as regular ImageView
super.invalidateDrawable(dr);
}
}

这里调用了父类ImageView的invalidateDrawable方法,刷新了ImageView的内容。

以上就是LottieAnimationView的playAnimation的整个过程。

下面给出一个整体的时序图,序号表明了调用顺序:

以上就是LottieAnimationView的playAnimation的整个过程。那么会有一个问题,动画是哪里来的,Lottie如何完成从一个json文件到实现动画的?

实际上,关键就在于 lottieView.setComposition(composition) 这一句代码上,参数composition是解析json动画文件后返回的,在lottieAnimationView.setComposition() 中,又调用了 lottieDrawable.setComposition(),看一下这个方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean setComposition(LottieComposition composition) {
if (this.composition == composition) {
return false;
}

isDirty = false;
clearComposition();
this.composition = composition;
buildCompositionLayer();
animator.setComposition(composition);
setProgress(animator.getAnimatedFraction());
setScale(scale);
updateBounds();

// We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
// then there won't be two iterators iterating and removing at the same time.
Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator();
while (it.hasNext()) {
LazyCompositionTask t = it.next();
t.run(composition);
it.remove();
}
lazyCompositionTasks.clear();

composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);

return true;
}

这段代码中有两个关键的部分,一个是buildCompositionLayer() 方法, 还有一个可以看一下while循环内部,我们前面在看playAnimation的源码的时候,当composition为null时,会把当前要执行的动作添加进一个lazyCompositionTask中,它就是在这里执行的,因为这时候已经给composition赋值了。然后看一下buildCompositionLayer()的源码:

1
2
3
4
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
}

可以看到这里初始化了一个CompositionLayer,用的是composition这个对象,先了解一下CompositionLayer是什么:实际上CompositionLayer就是一个布局的图层,它内部有一个drawLayer方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
L.beginSection("CompositionLayer#draw");
canvas.save();
newClipRect.set(0, 0, layerModel.getPreCompWidth(), layerModel.getPreCompHeight());
parentMatrix.mapRect(newClipRect);

for (int i = layers.size() - 1; i >= 0 ; i--) {
boolean nonEmptyClip = true;
if (!newClipRect.isEmpty()) {
nonEmptyClip = canvas.clipRect(newClipRect);
}
if (nonEmptyClip) {
BaseLayer layer = layers.get(i);
layer.draw(canvas, parentMatrix, parentAlpha);
}
}
canvas.restore();
L.endSection("CompositionLayer#draw");
}

从这个方法可以看出,它是包含了一个layer(图层)的集合layers,然后调用了每一个layer的draw方法。从这里我们就能初步发现,实际上Lottie的动画就是由一个一个的layer组成的,通过一层一层的layer,最终实现了Lottie动画,我们这里先给出一个总结,在下一章会详细分析动画的组成,以及动画json文件的分析。