图片来自必应
简介:Lottie是可以将AE做出的动画,通过
Bodymovin
输出为一个json文件,然后Lottie可以将这个json文件渲染为本地动画,包括Android、iOS、React native、还包括PC端也可以使用。使用LottieView,我们就可以很方便的实现一些复杂的动画,而且也不需要各端自己实现,或者说出现各端实现出来之后效果不一样的情况。如果使用LottieView的话,我们仅仅需要设计师输出一份json文件就可以了。
首先看一下LottieView官方网站上的一些动画的效果:
可以看出这些动画都有很酷炫的效果,如果要通过代码去实现的话就会非常的复杂,但是如果使用Lottie去加载,则非常简单,只需要几行代码就可以搞定,而且帧率大多都会稳定在60FPS。
基本使用
使用方式很简单,只需要在xml文件中声明一个LottieAnimationView
( 当然它还有一些属性可以在xml中设置,这里不再赘述,属性名称都能看的出来其作用),然后在代码中设置:
LottieCompositionFactory.fromAsset(context, "assertName").addListener{
lottieView.setComposition(it)
lottieView.playAnimation()
}.addFailureListener{
//Load Error
}
只需要上面这样几行代码,就可以实现复杂的动画了。可以看出首先是去assert中加载了一个json动画的资源,加载成功后会调用Listener,给LottieView设置Composition
,然后调用playAnimation()
方法播放就可以了,至于Composition
是什么,我们后面会说到。
源码解析
我们从 lottieView.playAnimation()
开始,因为它是开始播放的方法。那么跟踪源码就能看到:
/**
* 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()
这个方法做了什么:
@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:
public void playAnimation() {
running = true;
notifyStart(isReversed());
setFrame((int) (isReversed() ? getMaxFrame(·) : getMinFrame()));
lastFrameTimeNs = 0;
repeatCount = 0;
postFrameCallback();
}
可以看出,这个方法中调用了三个重要的方法:notifyStart(isReversed())、 setFrame()、和postFrameCallback().
其中notifyStart主要是开始动画,然后看一下setFrame都做了什么:
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中:
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};
最终是调用了compositionLayer的setProgress方法:
@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方法:
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,
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的变化:
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:
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
调用了onValueChanged,在动画的每一个图层都有实现,最终会调用到lottieDrawable.invalidateSelf()。 这里先强调一个图层的概念,之后回说到。然后这个invalidateSelf() 会调用 LottieAnimationView的invalidateDrawable()方法:
@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()
,看一下这个方法的实现:
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()
的源码:
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
}
可以看到这里初始化了一个CompositionLayer,用的是composition这个对象,先了解一下CompositionLayer是什么:实际上CompositionLayer就是一个布局的图层,它内部有一个drawLayer方法:
@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文件的分析。