lottie是由Airbnb发布的一款针对移动端、PC端、Web端都适用的开源动画库,它是通过AE设计师设计好动画效果,生成json文件,开发者只需要直接将json丢给lottie库,即可实现炫酷动画效果,节省了很多开发工作量。
近期工作正好涉及到lottie动画相关的开发,于是clone了一份源码进行学习分析,这里对源码进行分析实现总结(针对安卓端)
源码地址看这里:airbnb/lottie-android
基本使用:
- 通过布局方式,自动播放
hello_world
动画
- 通过LottieAnimationView,控制播放动画
LottieAnimationView lottieAnimationView = findViewById(R.id.lottieAnimationView);
lottieAnimationView.setAnimation("hello_world.json");
lottieAnimationView.playAnimation();
//...
- 通过LottieDrawable,设置到View上,也可控制播放动画
ImageView ivLottie = findViewById(R.id.iv_lottie);
LottieDrawable lottieDrawable = new LottieDrawable();
LottieCompositionFactory.fromAsset(this, "AndroidWave.json").addListener(result -> {
lottieDrawable.setComposition(result);
lottieDrawable.playAnimation();
});
ivLottie.setImageDrawable(lottieDrawable);
主要实现类
Lottie源码目录结构如下:
通过上面使用示例可以看出,LottieAnimationView是对开发者暴露的封装,核心的动画逻辑配置实现是在LottieDrawable进行的。
分析其几个主要实现类:
LottieAnimationView
继承自ImageView
封装动画操作设置接口,由接入方直接在布局文件中或代码中使用
LottieDrawable
lottie动画绘制的具体实现,替代LottieAnimationView的父类ImageView的drawable。
compositionLayer
继承自抽象类BaseLayer
封装json动画的图层的具体绘制过程,主要用于绘制特定图层以及子图层
LottieValueAnimator
继承自ValueAnimator
通过控制属性动画的具体实现
LottieCompositionFactory
LottieComposition的解析工厂,提供一系列解析lottie动画资源的方法,最终解析json生成LottieComposition
LottieImageAssets
lottie图片动画资源中的图片信息的数据模型,包括基本的宽高、对应图片、id等信息,也是图片bitmap化后的存储对象。
LottieTask
内部维护一个ThreadExcutor,用于管理lottie动画解析的task,对外抛出相应事件。
LottieDrawable
lottie动画绘制的具体实现,替代LottieAnimationView的父类ImageView的drawable。
ImageAssetManager
针对带图片的lottie动画图片资源管理类,图片最终以bitmap形式存储在hashmap中,且通过强引用方式,所以内存占用较大。
LottieAndroid动画驱动绘制流程
阅读安卓端源码可以看出,Lottie动画运行驱动是基于ValueAnimator,动画数据源基于从lottie json数据解析的结果Composition对象,ValueAnimator通过不断对外抛出valueChange事件,来让动画的layer不断完成绘制,并将绘制内容抛出到Drawable上,再通过View渲染,从而实现json数据到动画动起来的效果。
这里根据源码实现,绘制了LottieAndroid主流程的时序图:
优劣势:
- 节省开发成本,这点毋庸置疑,省去了动画实现的复杂开发过程。
- 复杂动画实现更高效,复杂动画在安卓端目前还没有最优解决方案,lottie应该算是靠前了。
- 内存占用高,当播放带图片资源的lottie动画时,源码中直接将图片转化为强引用Bitmap存在内存,没有做优化处理,如果图片资源过多,容易OOM。
- Lottie 动画资源设计导出需要设计师有一定的经验,在我工作中对接的设计师,导出的动画好几次存在漏图的问题,增加了沟通成本。
- ...
主流程部分源码
lottie通过先设置,后启动的方式播放动画,设置的属性用于控制动画的播放状态, 我们可以从lottie关联到json动画的源码处入手:
lottieAnimationView.setAnimation("hello_world.json");
class LottieAnimationView{
//LottieAnimationView
private final LottieListener loadedListener = this::setComposition;
/**
* Sets a composition.
* You can set a default cache strategy if this view was inflated with xml by
* using {@link R.attr#lottie_cacheComposition}.
*/
public void setComposition(@NonNull LottieComposition composition) {
//...
boolean isNewComposition = lottieDrawable.setComposition(composition);
//...
setLottieDrawable();
//...
requestLayout();
//...
}
public void setAnimation(final String assetName) {
this.animationName = assetName;
animationResId = 0;
setCompositionTask(fromAssets(assetName));
}
private void setCompositionTask(LottieTask compositionTask) {
userActionsTaken.add(UserActionTaken.SET_ANIMATION);
clearComposition();
cancelLoaderTask();
this.compositionTask = compositionTask
.addListener(loadedListener)
.addFailureListener(wrappedFailureListener);
}
}
//LottieTask
public class LottieTask {
//...
public static Executor EXECUTOR = Executors.newCachedThreadPool();
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public LottieTask(Callable> runnable) {
this(runnable, false);
}
/**
* runNow is only used for testing.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Callable> runnable, boolean runNow) {
if (runNow) {
try {
setResult(runnable.call());
} catch (Throwable e) {
setResult(new LottieResult<>(e));
}
} else {
EXECUTOR.execute(new LottieFutureTask(runnable));
}
}
//...
可以看出,在设置动画资源时,将json文件的解析过程封装成了LottieTask的形式,查看LottieTask实现,可以看出,其内部维护了线程池,并且其构造函数也可以看出,可以在初始化时,立即将解析任务放入线程池。在得到解析结果后,又会回调LottieAnimationView的setComposition方法,设置Composition。
前面已经知道,Composition对象就是json动画文件的代码化表达方式。LottieAndroid做的就是将Composition消费成动画。
这里,在LottieAnimationView的setComposition方法中,可以看到调用了LottieDrawable的setComposition方法,同时下一步通过setDrawable将drawable与LottieAnimationView关联。那么,就看下LottieDrawable的实现。
public class LottieDrawable{
ArrayList lazyCompositionTasks = new ArrayList<>();
LottieValueAnimator animator = new LottieValueAnimator();
public boolean setComposition(LottieComposition composition) {
//...
buildCompositionLayer()
}
private void buildCompositionLayer() {
LottieComposition composition = this.composition;
if (composition == null) {
return;
}
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
if (outlineMasksAndMattes) {
compositionLayer.setOutlineMasksAndMattes(true);
}
compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
}
@MainThread
public void playAnimation() {
//...
animator.playAnimation();
onVisibleAction = OnVisibleAction.NONE;
// ...
}
}
LottieDrawable负责具体的动画实现,这里初始化动画数据就是setComposition函数做的事,主要是需要根据LottieComposition 构建出图层数据(CompositionLayer),Layer在json数据中也可以看出这个节点,lottie的动画绘制主要是递归Layer进行draw。
图层数据有了之后,就算是动画数据准备好了,直接调用playAnimation,即可触发动画播放了。playAnimation触发了LottieValueAnimation的playAnimation。
public abstract class BaseLottieAnimator extends ValueAnimator {
void notifyUpdate() {
for (AnimatorUpdateListener listener : updateListeners) {
listener.onAnimationUpdate(this);
}
}
}
LottieAnimator 在运行过程中,不断对外抛出onAnimationUpdate事件。
public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};
}
LottieDrawable 接收到onAnimationUpdate事件,又将其事件值转化为progress,抛给compostionLayer:
public class CompositionLayer extends BaseLayer {
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
//...
if (mask != null) {
for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
mask.getMaskAnimations().get(i).setProgress(progress);
}
}
if (inOutAnimation != null) {
inOutAnimation.setProgress(progress);
}
if (matteLayer != null) {
matteLayer.setProgress(progress);
}
for (int i = 0; i < animations.size(); i++) {
animations.get(i).setProgress(progress);
}
// ...
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
}
ComposeLayer又将progresschange事件抛给动画子元素,层层判断是否需要处理progress,类似点击事件传递。
public abstract class BaseKeyframeAnimation {
final List listeners = new ArrayList<>(1);
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
// ...
if (keyframesWrapper.isValueChanged(progress)) {
notifyListeners();
}
}
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
}
其中的一个动画帧元素消费progress的逻辑,可以看出,这里又对外抛出onValueChanged事件。跟踪代码,onValueChanged接收者消费这个事件的逻辑都是一致的,最终调用的都是:
class BaseLayer implements DrawingContent, BaseKeyframeAnimation.AnimationListener, KeyPathElement {
@Override
public void onValueChanged() {
invalidateSelf();
}
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}
}
根据View的绘制逻辑,会由LottieDrawable执行绘制,也就是会调用LottieDrawable的draw方法。LottieDrawable的draw又会调用compositionLayer的draw方法。
public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
public void draw(@NonNull Canvas canvas) {
//...
compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
//...
}
}
最终,在CompositionLayer中完成了当前动画帧的绘制,循环往复,也就呈现了动画效果。