一 原理概述
1.1 核心类
LottieAnimationView
扩展了 ImageView,是加载 Lottie 动画的默认且最简单的方法。LottieDrawable
具有与 LottieAnimationView 相同的大部分 API,但是您可以在任何您想要的视图上使用它。LottieComposition
组合是动画的无状态模型表示。只要您需要,这个文件可以安全地缓存很长时间,并且可以在drawables/views之间自由地重用。LottieCompositionFactory
允许您从许多输入创建 LottieCompostionFactory。这就是 LottieDrawable 和 LottieAnimationView 上的 setAnimation (…) API 在底层使用的。工厂方法也与这些类共享相同的缓存。
1.2 概述
LottieAnimationView继承自ImageView,通过当前时间绘制canvas显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:
由上述过程可以看到,Lottie解析并展示动画的流程主要可分为三步:解析Json为LottieComposition;LottieComposition转为LottieDrawer;LottieDrawer播放动画;下面来一步步解析:
二 解析Josn文件
进入setCompositionTask方法:
public void setAnimation(@RawRes final int rawRes) {
…
setCompositionTask(fromRawRes(rawRes));
}
进入fromRawRes方法:
private LottieTask<LottieComposition> fromAssets(final String assetName) {
if (isInEditMode()) {
//避免可视化编辑报错问题
...
} else {
//cacheComposition记录是否已缓存
//最终拿到json文件的LottieComposition数据模型
return cacheComposition ?
LottieCompositionFactory.fromAsset(getContext(), assetName) : LottieCompositionFactory.fromAsset(getContext(), assetName, null);
}
}
进入LottieCompositionFactory.fromAsset方法,最终一连串函数调用,走到LottieCompositionMoshiParser
.parse方法:
// 对字节流内容进行解析:
LottieComposition composition = LottieCompositionMoshiParser
.parse(reader);
if (cacheKey != null) {
LottieCompositionCache.getInstance().put(cacheKey, composition);
}
parse过程需要对json的每个字段进行映射对应,并进行解析,映射关系如下:
// 解析json字段:
private static final JsonReader.Options NAMES = JsonReader.Options.of(
"w", // 0
"h", // 1
"ip", // 2
"op", // 3
"fr", // 4
"v", // 5
"layers", // 6
"assets", // 7
...
);
Options是JsonReader的 static 内部类,of是方法;
四 生成LottieDrawer
LottieAnimationView将解析后生成的LottieComposition对象传递给LottieDrawer;
/**
* 设置一个composition.
* 如果这个视图使用R.attr.lottie_cacheComposition填充xml,则可以设置默认缓存策略。
*/
public void setComposition(@NonNull LottieComposition composition) {
if (L.DBG) {
Log.v(TAG, "Set Composition \n" + composition);
}
lottieDrawable.setCallback(this);
this.composition = composition;
ignoreUnschedule = true;
//将解析后的LottieComposition传递给LottieDrawable
boolean isNewComposition = lottieDrawable.setComposition(composition);
ignoreUnschedule = false;
enableOrDisableHardwareLayer();
if (getDrawable() == lottieDrawable && !isNewComposition) {
// We can avoid re-setting the drawable, and invalidating the view, since the composition
// hasn't changed.
//我们可以避免重新设置drawable,并使视图无效,因为合成并没有改变。
return;
} else if (!isNewComposition) {
// The current drawable isn't lottieDrawable but the drawable already has the right composition.
// 当前的drawable不是lottieDrawable,但drawable已经有正确的组成。
setLottieDrawable();
}
// This is needed to makes sure that the animation is properly played/paused for the current visibility state.
// 需要确保动画在当前可见状态是正确播放/暂停。
// It is possible that the drawable had a lazy composition task to play the animation but this view subsequently
// became invisible. Comment this out and run the espresso tests to see a failing test.
onVisibilityChanged(this, getVisibility());
requestLayout();
for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) {
lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
}
}
LottieDrawable将LottieComposition对象构造为CompositionLayer
public boolean setComposition(LottieComposition composition) {
if (this.composition == composition) {
return false;
}
isDirty = false;
clearComposition();
this.composition = composition;
buildCompositionLayer();
...
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
if (outlineMasksAndMattes) {
compositionLayer.setOutlineMasksAndMattes(true);
}
}
CompositionLayer继承自Baselayer,并且在构造时会遍历所有layer图层,转换为BaseLayer对象。
public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
LottieComposition composition) {
super(lottieDrawable, layerModel);
...
LongSparseArray<BaseLayer> layerMap =
new LongSparseArray<>(composition.getLayers().size());
BaseLayer mattedLayer = null;
for (int i = layerModels.size() - 1; i >= 0; i--) {
Layer lm = layerModels.get(i);
BaseLayer layer = BaseLayer.forModel(this, lm, lottieDrawable, composition);
...
}
}
这里通过BaseLayer的forModel方法,将BaseLayer的各个子类型抽象出来
@Nullable
static BaseLayer forModel(
CompositionLayer compositionLayer, Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
switch (layerModel.getLayerType()) {
case SHAPE:
return new ShapeLayer(drawable, layerModel, compositionLayer);
case PRE_COMP:
return new CompositionLayer(drawable, layerModel,
composition.getPrecomps(layerModel.getRefId()), composition);
case SOLID:
return new SolidLayer(drawable, layerModel);
case IMAGE:
return new ImageLayer(drawable, layerModel);
case NULL:
return new NullLayer(drawable, layerModel);
case TEXT:
return new TextLayer(drawable, layerModel);
case UNKNOWN:
default:
// Do nothing
Logger.warning("Unknown layer type " + layerModel.getLayerType());
return null;
}
}
以下是Lottie的不同layer类型:
到这里,LottieDrawable就通过CompositionLayer将各个类型的layer实例化,然后在LottieDrawable的draw()方法中完成所有图层的绘制:
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
CompositionLayer compositionLayer = this.compositionLayer;
if (compositionLayer == null) {
return;
}
compositionLayer.draw(canvas, matrix, alpha);
}
五 播放动画
通过LottieAnimationView的playAnimation方法可以看到,内部会调用LottieDrawable的playAnimation方法,然后会触发LottieValueAnimator的playAnimation方法。LottieValueAnimator实际也是一个ValueAnimator,所以本质上Lottie也是属性动画驱动的。
具体在LottieDrawable中可以看到,LottieValueAnimator调用updateListener后,会刷新CompositionLayer的progress。
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};
进入setProgress可以看到,CompositionLayer会遍历所有layer图层,并逐个调用其setProgress方法。
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
...
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
进入BaseLayer的setProgress方法会发现,会调用所有BaseKeyframeAnimation的setProgress方法,并会在BaseLayer中回调调用invalidateSelf()方法。
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}
回调invalidateSelf()方法后,LottieDrawable会回调draw方法
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
CompositionLayer compositionLayer = this.compositionLayer;
if (compositionLayer == null) {
return;
}
compositionLayer.draw(canvas, matrix, alpha);
}
进入CompositionLayer的draw方法
@Override
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
L.beginSection(drawTraceName);
if (!visible || layerModel.isHidden()) {
L.endSection(drawTraceName);
return;
}
buildParentLayerListIfNeeded();
L.beginSection("Layer#parentMatrix");
matrix.reset();
matrix.set(parentMatrix);
for (int i = parentLayers.size() - 1; i >= 0; i--) {
matrix.preConcat(parentLayers.get(i).transform.getMatrix());
}
L.endSection("Layer#parentMatrix");
int opacity = transform.getOpacity() == null ? 100 : transform.getOpacity().getValue();
int alpha = (int)
((parentAlpha / 255f * (float) opacity / 100f) * 255);
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
matrix.preConcat(transform.getMatrix());
L.beginSection("Layer#drawLayer");
drawLayer(canvas, matrix, alpha);
L.endSection("Layer#drawLayer");
recordRenderTime(L.endSection(drawTraceName));
return;
}
……
实际这样构成了一个循环,随着animator动画的进行,LottieDrawable会不断的绘制,这样Lottie动画就跑起来了,流程图如下:
六 总结
首先会通过LottieCompositionFactory的对应类型设置json资源文件;
然后再fromJsonSync方法里面会把json文件解析出图层的大小并且绘制相应的图片资源文件和图层;
资源加载完后,会在回调里面设置LottieAnimationView的Composition,从而调用LottieDrawable的setComposition()方法
在setComposition方法里面会通过buildCompositionLayer()方法去创建一个CompositionLayer图层,其中CompositionLayer继承BaseLayer,通过BaseLayer的forModel()静态方法获取不同的图层类型;
然后LottieDrawable的setComposition()方法里面会开始执行一个ValueAnimation动画,这个动画会驱使BaseLayer的draw()方法不断执行,通过Matrix的矩阵形式不断的绘制各个图层从而形成动画,而这些图层的矩阵变换的数据来源于BaseKeyframeAnimation里面有一个Keyframe对象会去Json里面获取相应得数据。
附图(参考):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rv4eJcr5-1658136002844)(/Users/a58/Library/Application Support/typora-user-images/image-20220718145339827.png)]
源码
例子源码 : https://github.com/LucasXu01/lottiedemo