Lottie
json文件到对象的映射
- LottieCompositionFactory类提供了多个静态方法用于从不同途径加载 json 数据并将其解析为
LottieComposition - 以 “Sync” 结尾的方法将在调用线程以同步的方式加载并解析 json 数据;其余方法则通过 Lottie 自定义的
AsyncTask子类AsyncCompositionLoader异步地加载并解析 json 数据。 - 无论是以异步还是同步的方式,Lottie 均使用
JsonReader来解析 json 数据,从而避免了因一次性将全部 json 数据加载到内存之中而带来的 OOM 问题。 - 实际执行 json 解析的是
LottieCompositionParser的parse方法。该方法中将 json 中的字段一一解析为LottieComposition的成员变量,进而构造了一个LottieComposition实例
解析外部资源
{
"v": "5.1.10", //bodymovin的版本
"fr": 24, //帧率
"ip": 0, //起始关键帧
"op": 277, //结束关键帧
"w": 110, //动画宽度
"h": 110, //动画高度
"nm": "合成 2",
"ddd": 0,
"assets": [...] //资源信息
"layers": [...] //图层信息
}
在最外层中,可以获取插件 bodymovin 的版本号,起始关键帧,动画帧率及动画的宽高等属性。
解析图片
{
"id": "image_0", //图片id
"w": 750, //图片宽度
"h": 1334, //图片高度
"u": "images/", //图片路径
"p": "img_0.png" //图片名称
}
图片资源是在 “asset” 里边的,可以有多张图片,每张图片数据解析后会转换为实体类 LottieImageAsset
解析图层
"layers": [
{
"ddd": 0,
"ind": 1, //图层 id
"ty": 2, //图层类型
"nm": "eye-right 2",
"parent": 3,
"refId": "image_0", //引用资源Id
"sr": 1,
"ks": { //动画属性值
"s": { //s:缩放的数据值
"a": 0,
"k": [
100,
100,
100
],
"ix": 6
}...},
"ip": 0, //inFrame 该图层起始关键帧
"op": 241, //outFrame 该图层结束关键帧
"st": 0, //startFrame 开始关键帧
"bm": 0
},
一张复杂的图片可以使用多个图层来表示,每个图层展示一部分内容,图层中的内容也可以拆分为多个元素。在 Lottie 解析的 Json 中,图层可以分为以下几种:PreComp,Solid,Image,Null,Shape,Text。在数据解析时,先会转换为数据实体类 Layer,根据 type 来区分是哪种图层。
从 LottieCompisition 到 CompositionLayer
使用 Lottie 第二步是设置数据对象 LottieComposition,把数据对象的数据转换为具有绘制能力的 BaseLayer,以下是转换的部分类图:
不足
1. 有 mask、matters 时,有很大的性能影响
可以查看源码在 BaseLayer,是如何对有 mask 或 matte 进行计算的:
如果不含 mask 或 matte,这里直接调用 drawLayer 就返回了,但是对于 mask 或 matte 会先进行 saveLayer,再去绘制 drawLayer。
在 saveLayer 中,可以看到这是个非常耗时的操作,需要分配和绘制一个 offscreen 的缓冲区,渲染的成本增加了一倍以上。
所以这里 mask 或 matte 的边界越小,性能损耗也会越小。
2. 解码图片在主线程
查看 Lottie 获取图片的 ImageAssetManager 源码:
@Nullable public Bitmap bitmapForId(String id) {
Bitmap bitmap = bitmaps.get(id);
if (bitmap == null) {
LottieImageAsset imageAsset = imageAssets.get(id);
if (imageAsset == null) {
return null;
}
if (delegate != null) {
bitmap = delegate.fetchBitmap(imageAsset);
if (bitmap != null) {
bitmaps.put(id, bitmap);
}
return bitmap;
}
InputStream is;
try {
if (TextUtils.isEmpty(imagesFolder)) {
throw new IllegalStateException("You must set an images folder before loading an image." +
" Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder");
}
is = context.getAssets().open(imagesFolder + imageAsset.getFileName());
} catch (IOException e) {
Log.w(L.TAG, "Unable to open asset.", e);
return null;
}
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = true;
opts.inDensity = 160;
bitmap = BitmapFactory.decodeStream(is, null, opts);
bitmaps.put(id, bitmap);
}
return bitmap;
}
这里如果没有设置图片代理,则直接解码图片。如果有设置图片代理,则在代理监听里获取图片。
mLottieAnim.setImageAssetDelegate(new ImageAssetDelegate() {
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
try {
FileInputStream fileInputStream = new FileInputStream("/sdcard/data/images/" + asset.getFileName());
return BitmapFactory.decodeStream(fileInputStream);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
});
这俩种方式,都将在主线程中调用,第一次都会从文件中解码,之后会直接从缓存中获取。对于一些 Android 低端机可能无法播放动画,甚至引发 ANR;
可能 Lottie 更多考虑的是没有图片或图片很少的情况,比如一些矢量动画,这种性能会很好。
数据对象到Drawable的映射
AnimatableLayer 继承自 Drawable,我们看下它的子类:
其中LayerView对应着Layer数据,Layer中有:
Lottie播放流程
绘制
LottieAnimationView 继承自 AppCompatImageView,封装了一些动画的操作,如:
具体的绘制时委托为 LottieDrawable 完成的,我们看下 LottieDrawable 中的 draw() 方法:
LottieDrawable 继承自AnimatableLayer,其draw()方法如下:
可以看到先绘制了本层的内容,然后开始绘制包含的layers的内容: