lottie源码分析
lottie简介
Lottie是Airbnb开源的一个动画渲染库,同时支持Android、IOS、React Native和Web平台,Lottie目前只支持渲染播放AE动画。Lottie使用bobymovin(After Effects插件)导出的json数据作为动画数据源。
image imagelottie的优缺点
优点:
- 相对与矢量动画,lottie的操作更为简单,生成文件的操作不需要程序猿完成,而且AE相对于一些矢量图制作工具更加强大效果更好
矢量图在线制作工具 https://shapeshifter.design - 使用GIF,使用帧动画占用空间大,Android原生不支持GIF动画的显示。
- 组合式动画,通过大量代码实现复杂的动画效果,代码复杂,不好调试,也会浪费很多时间成本
- Android, iOS, 和React Native多平台支持
- 降低动画设计和开发成本
- 完美解决设计提供动画效果与实现不一致问题
- 不需要ui适配
缺点:依然有局限性,对于一些复杂的动画特效,如高斯模糊等无法实现,可能是由于json文件不好描述
框架原理
使用AE工具生成一段json,Lottie使用json文件来作为动画数据源,然后解析json数据,根据解析后的数据建立合适的Drawable绘制到View上面。
使用
private void play(String name){
// 取消播放
mAnimationView.cancelAnimation();
// 是否循环播放
mAnimationView.loop(true);
// 设置播放速率,例如:2代表播放速率是不设置时的二倍
//mAnimationView.setSpeed(2f);
// 开始播放
mAnimationView.playAnimation();
// 暂停播放
mAnimationView.pauseAnimation();
// 设置播放进度
//mAnimationView.setProgress(0.5f);
// 判断是否正在播放
// mAnimationView.isAnimating();
mAnimationView.setAnimation(name);
mAnimationView.loop(false);
mAnimationView.playAnimation();
}
/**
* 自定义播放动画和时长
*/
private void playValueAnimator(){
ValueAnimator valueAnimator = ValueAnimator
.ofFloat(0f, 1f)
.setDuration(5000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationView.setProgress((Float) animation.getAnimatedValue());
}
});
valueAnimator.start();
}
关于协议
{
"v": "4.11.1", //使用bodymovin的版本
"fr": 60, //帧率
"ip": 0, //起始关键帧
"op": 180, //结束关键帧
"w": 300, //视图的宽度 宽高会根据屏幕密度做转换成scaleWidth
"h": 300, //视图的高度
"nm": "Comp 1", //从源码中未看到对此字段解析
"ddd": 0,
"assets": [], //图片集合
"layers": [ //图层集合
{
"ddd": 0,
"ind": 1, //layer的Id,唯一
"ty": “sh", //layer的类型
"nm": "Shape Layer 1", //layer的名称,在ae中生成唯一
"sr": 1,
"ks": {}, //外观信息
"ao": 0,
"shapes": [], //矢量图形图层的数组
"ip": 0, // 该图层的起始关键帧
"op": 180, //该图层的结束关键帧
"st": 0,
"bm": 0
},
{...},
{...},
{...},
]
}
ks中的字段
- a 位置信息
- p 位移信息
- s 缩放信息
- r 翻转信息
- o 不透明度
- so 开始时不透明度
- eo 结束时不透明度
源码解析
一个动画文件的播放过程大概可以分为三部分
- 解析json文件
- view绘制
- 动画播放
解析json文件
从setAnimation方法点进来,看到在执行解析asset文件夹下文件
public void setAnimation(final String assetName) {
this.animationName = assetName;
animationResId = 0;
setCompositionTask(LottieCompositionFactory.fromAsset
(getContext(), assetName));
}
LottieCompositionFactory这个类有很多解析方法包括raw,asset等文件夹下
public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
// Prevent accidentally leaking an Activity.
final Context appContext = context.getApplicationContext();
//如果之前缓存过,取缓存,线程同步的方法,会阻塞主线程
return cache(fileName, new
Callable<LottieResult<LottieComposition>>() {
@Override public LottieResult<LottieComposition> call() {
//该方法就是拿到了json文件的字节流
return fromAssetSync(appContext, fileName);
}
});
}
拿到文件的字节流后对内容进行解析
LottieComposition composition = LottieCompositionParser.parse(reader);
解析时会对LottieComposition进行赋值,拿到以下很多的字段
float scale = Utils.dpScale();
float startFrame = 0f;
float endFrame = 0f;
float frameRate = 0f;
final LongSparseArray<Layer> layerMap = new LongSparseArray<>();
final List<Layer> layers = new ArrayList<>();
int width = 0;
int height = 0;
Map<String, List<Layer>> precomps = new HashMap<>();
Map<String, LottieImageAsset> images = new HashMap<>();
Map<String, Font> fonts = new HashMap<>();
List<Marker> markers = new ArrayList<>();
SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();
LottieTask是一个线程池,LottieResult是LottieComposition的结果或者exception,监听回调中得到解析后composition数据结构
private final LottieListener<LottieComposition> loadedListener = new LottieListener<LottieComposition>() {
@Override public void onResult(LottieComposition composition) {
//得到解析后composition
setComposition(composition);
}
};
drawable的绘制
比较核心的两个类
LottieComposition和LottieDrawable将会在下面专门进行分析,他们分别进行了两个重要的工作:json文件的解析和动画的绘制。
LottieAnimationView中的setComposition讲数据结构交给了lottieDrawable
public void setComposition(@NonNull LottieComposition composition) {
if (L.DBG) {
Log.v(TAG, "Set Composition \n" + composition);
}
lottieDrawable.setCallback(this);
this.composition = composition;
//lottieDrawable对解析后composition数据做了加工
boolean isNewComposition =
lottieDrawable.setComposition(composition);
enableOrDisableHardwareLayer();
if (getDrawable() == lottieDrawable && !isNewComposition) {
return;
}
setImageDrawable(null);
setImageDrawable(lottieDrawable);
requestLayout();
for (LottieOnCompositionLoadedListener lottieOnCompositionLoadedListener : lottieOnCompositionLoadedListeners) { lottieOnCompositionLoadedListener.onCompositionLoaded(composition);
}
}
lottieDrawable中的setComposition方法中的buildCompositionLayer开始真正的解析layer和绘制
layer算是lottie原理中一个比较重要的概念,就是图层
layer的类型与 AE中的图层的对应关系为:
- ShapeLayer:形状图层
- CompositionLayer:预合成图层
- SolidLayer:纯色图层
- ImageLayer:图片素材图层
- NullLayer:空图层
- TextLayer:文本图层
在android层面可以理解为图层就是view,在一个布局viewGroup中有很多的view,就是不断的绘制这些view来完成这些动画的,LottieComposition对Layer进行数据的映射,在CompositionLayer中为每一个layer生成一个对应的LayerView
简单说就是解析json->layer对象的映射->layer对象为layerview构造出各种path等->数据全部准备好就是不断的驱使draw方法完成绘制
CompositionLayer中的构造方法
public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
LottieComposition composition) {
super(lottieDrawable, layerModel);
AnimatableFloatValue timeRemapping = layerModel.getTimeRemapping();
if (timeRemapping != null) {
this.timeRemapping = timeRemapping.createAnimation();
addAnimation(this.timeRemapping);
//noinspection ConstantConditions
this.timeRemapping.addUpdateListener(this);
} else {
this.timeRemapping = null;
}
//hashmap的优化数据结构
LongSparseArray<BaseLayer> layerMap =
new LongSparseArray<>(composition.getLayers().size());
BaseLayer mattedLayer = null;
//遍历layer图层
for (int i = layerModels.size() - 1; i >= 0; i--) {
Layer lm = layerModels.get(i);
BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
if (layer == null) {
continue;
}
layerMap.put(layer.getLayerModel().getId(), layer);
if (mattedLayer != null) {
mattedLayer.setMatteLayer(layer);
mattedLayer = null;
} else {
layers.add(0, layer);
switch (lm.getMatteType()) {
case ADD:
case INVERT:
mattedLayer = layer;
break;
}
}
}
//将layer生成各种layerView完成绘制
for (int i = 0; i < layerMap.size(); i++) {
long key = layerMap.keyAt(i);
BaseLayer layerView = layerMap.get(key);
// This shouldn't happen but it appears as if sometimes on pre-lollipop devices when
// compiled with d8, layerView is null sometimes.
// https://github.com/airbnb/lottie-android/issues/524
if (layerView == null) {
continue;
}
BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId());
if (parentLayer != null) {
layerView.setParentLayer(parentLayer);
}
}
}
父类中根据不同类型,绘制不同的图层
static BaseLayer forModel(
Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
switch (layerModel.getLayerType()) {
//形状图层,调用最频繁
case SHAPE:
return new ShapeLayer(drawable, layerModel);
//预合成图层
case PRE_COMP:
return new CompositionLayer(drawable, layerModel,
composition.getPrecomps(layerModel.getRefId()), composition);
//纯色图层
case SOLID:
return new SolidLayer(drawable, layerModel);
//有些会是zip压缩包中会有图片,在这里解析成bitmap
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
L.warn("Unknown layer type " + layerModel.getLayerType());
return null;
}
}
然后就是通过setImageDrawable(lottieDrawable)将图像显示出来,显示第一帧动画。
动画播放
通过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);
}
}
父类中layer通知进度的改变
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会更新BaseKeyframeAnimation.AnimationListener回调onValueChanged触发LottieDrawable重绘
//会调用invalidateSelf()方法,该方法会触发LottieAnimationView的invalidateDrawable,然后
animations.get(i).setProgress(progress);
}
}
BaseKeyframeAnimation.AnimationListener会粗发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);
}
}
在LottieDrawable的setComposition()的方法中会开始执行一个ValueAnimation动画,这个动画会驱使baseLayer的draw()方法不断执行
@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");
}
总结
- 创建 LottieAnimationView lottieAnimationView
- 在LottieAnimationView中创建LottieDrawable lottieDrawable
- 在LottieAnimationView中创建compositionLoader,进行json文件解析得到LottieComposition,完成数据到对象Layer的映射。
- 解析完后通过setComposition方法把LottieComposition给lottieDrawable,lottieDrawable在setComposition方法中转换成各种Layer为绘制做准备比如path,maritx
- 在LottieAnimationView中把lottieDrawable设置setImageDrawable
- 然后开始动画lottieDrawable.playAnimation()。
demo地址
源码中添加了很多注释
https://github.com/Johncuiqiang/LottieSource
参考
https://blog.csdn.net/weixin_37618354/article/details/84072783
https://blog.csdn.net/dcsff/article/details/80482841
https://blog.csdn.net/xiexiangyu92/article/details/78525456