Android•Lottie 动画库填坑记
入坑背景
由于从事直播软件开发的缘故,本猿在版本迭代过程中一期不落的接触到各式各样动画效果。最早的时候,苦逼的用 Android 原生动画做直播间全屏礼物,反复的看着美工给的 Flash 效果图,不断的拼凑素材图片,调整控制动画播放的属性值,各个动画代码都很类似,但却无法套用,一连两三天下来,基本上脑海中除了动画就一片空白
踩坑准备
熟悉一个新的框架最快的方式就是查看官方文档,因为官方文档中一般都会给出一个 Demo,果不其然,Lottie 也是!文档的阅读量不是很大,通篇下来介绍了:
播放本地 Assets 目录下的 Json 动画文件
通过 Json 数据播放动画
如何对动画进行监听以及动画进度调节
Lottie 动画数据的预加载和缓存
为 Assets 目录下的 Json 动画文件配置动画所需要的素材
开始入坑
然而,他介绍了这么多,并没有一款适合我的。因为服务器下发不是简单的 Json 数据,是一个动画压缩包,里面包括了动画文件和播放动画需要的素材文件,而且解压后的文件也不在 Asset 目录下。于是,只好跟踪 animationView.setAnimation("hello-world.json")源码,看看最终到底做了什么事!
public void setAnimation(String animationName) {
setAnimation(animationName, defaultCacheStrategy);
}
一个参数调用两个参数同名方法,只好接着往下看!
public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
this.animationName = animationName;
if (weakRefCache.containsKey(animationName)) {
WeakReference<LottieComposition> compRef = weakRefCache.get(animationName);
if (compRef.get() != null) {
setComposition(compRef.get());
return;
}
} else if (strongRefCache.containsKey(animationName)) {
setComposition(strongRefCache.get(animationName));
return;
}
this.animationName = animationName;
lottieDrawable.cancelAnimation();
cancelLoaderTask();
compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName,
new OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
if (cacheStrategy == CacheStrategy.Strong) {
strongRefCache.put(animationName, composition);
} else if (cacheStrategy == CacheStrategy.Weak) {
weakRefCache.put(animationName, new WeakReference<>(composition));
}
setComposition(composition);
}
});
}
从这里可以看到官方文档中说的缓存,包括强引用缓存,弱引用缓存,和无缓存模式,而且知道 Json 动画文件最终会转化为 Composition 对象,而 Compostion 对象是通过 LottieComposition
public static Cancellable fromAssetFileName(Context context, String fileName,
OnCompositionLoadedListener loadedListener) {
InputStream stream;
try {
stream = context.getAssets().open(fileName);
} catch (IOException e) {
throw new IllegalStateException("Unable to find file " + fileName, e);
}
return fromInputStream(context, stream, loadedListener);
}
看到这里我们这就明白,当初传入的文件名,最终还是通过getAssets().open(fileName) 的方法,以流的方式进行处理了,于是我们可以这样加载放在其他目录下的 Json 动画文件。
public static void loadAnimationByFile(File file, final OnLoadAnimationListener listener) {
if (file == null || !file.exists()) {
if (listener != null) {
listener.onFinished(null);
}
return;
}
FileInputStream fins = null;
try {
fins = new FileInputStream(file);
LottieComposition.Factory.fromInputStream(GlobalContext.getAppContext(), fins, new OnCompositionLoadedListener() {
@Override
public void onCompositionLoaded(LottieComposition composition) {
if (listener != null) {
listener.onFinished(composition);
}
}
});
} catch (IOException e) {
e.printStackTrace();
if (listener != null) {
listener.onFinished(null);
}
if (fins != null) {
try {
fins.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}
异步的方式获取 Composition 对象,因为不使用 setAnimation(final String animationName, final CacheStrategy cacheStrategy) 方法,所以我们没法使用框架提供的缓存,为了下次播放时不需要重新解析动画文件,使动画的加载速度更快,我们也需要重新做一套缓冲处理,如下
LocalLottieAnimUtil.loadAnimationByFile(animFile, new LocalLottieAnimUtil.OnLoadAnimationListener() {
@Override
public void onFinished(LottieComposition lottieComposition) {
if (lottieComposition != null) {
mCenter.putLottieComposition(id, lottieComposition); // 使用
} else {
GiftFileUtils.deleteFile(getAnimFolder(link)); //删除动画文件目录,省的下次加载依然失败,而是重新去下载资源压缩包
}
public class EnterRoomResCenter {
private SparseArray<LottieComposition> lottieCompositions = new SparseArray<>(); //缓存Composition
public void putLottieComposition(int id, LottieComposition composition) {
lottieCompositions.put(id, composition);
}
public LottieComposition getAnimComposition(int id) {
return mCenter.getLottieComposition(id);
}
}
完成了 Json 动画文件的加载,接下来就是播放动画。正如源码方法中 setAnimation(final String animationName, final CacheStrategy cacheStrategy) 一样,我们也需要对 LottieAnimationView 进行setComposition(composition) 处理,然后调用LottieAnimationView.playAnimation() 就可以进行动画播放了,于是我这样做了:
public static void playAnimation(LottieAnimationView animationView,LottieComposition composition) {
animationView.setComposition(composition);
animationView.playAnimation();
}
想想这个需求马上就要搞定,于是我抿抿嘴偷偷笑了,这也太轻松了吧!于是端起茶杯去接了杯水,并运行了项目,准备回来看到那绚丽的动画。然而,事与愿违,等待我的是一片血红的“大姨妈”。
java.lang.IllegalStateException:
You must set an images folder before loading an image. Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder
看到这个错误,想起官方文档上面有说,如何为动画配置播放动画所需要的素材,而且错误提示也特别的明显,看了看给的资源包的目录,似乎发现了什么!于是我按照官方《为 Assets 目录下的 Json动画文件设置播放动画所需要的资源》一样,改了一下代码:
public static void playAnimation(LottieAnimationView animationView,String imageFolder, LottieComposition composition) {
animationView.setComposition(composition);
animationView.setImageAssetsFolder(imageFolder); // 新添加的
animationView.playAnimation();
}
想着异常信息都提示这么明显了,而且官方文档给的模板也是这样写的,我更加确定这次动画播放绝对的没有问题。然而,动画最终还是没有播放出来!没办法,只好继续翻源码,既然 Assets 目录下setImageAssetsFolder
@SuppressWarnings("WeakerAccess") public void setImageAssetsFolder(String imageAssetsFolder) {
lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);
}
没有什么头绪只好继续往下看:
@SuppressWarnings("WeakerAccess") public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
this.imageAssetsFolder = imageAssetsFolder;
}
这个变量被设置成类属性了,那么我们只需要在这个类下搜索怎么样被使用就可以马上定位出原因,发现有这么一行:
imageAssetBitmapManager = new ImageAssetBitmapManager(getCallback(),
imageAssetsFolder, imageAssetDelegate, composition.getImages());
}
我擦,变量被传递到一个 ImageAssetBitmapManager 对象里面去了,只好进这个类继续跟踪,最终定位到这样一个方法:
Bitmap bitmapForId(String id) {
Bitmap bitmap = bitmaps.get(id);
if (bitmap == null) {
LottieImageAsset imageAsset = imageAssets.get(id);
if (imageAsset == null) {
return null;
}
if (assetDelegate != null) {
bitmap = assetDelegate.fetchBitmap(imageAsset);
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;
}
播放动画所需要的图片资源都通过这个方法获取,传入一个图片文件名称,然后通过流获取 Bitmap 对象并返回。这里需要介绍一下:
如果 Json 动画文件使用了图片素材,里面的 Json 数据必然会声明该图片文件名。在 Composition.Factory 进行解析为 Composition 时,里面使用的图片都以键值对的方式存放到 Composition 的
private final Map<String, LottieImageAsset> images = new HashMap<>() 中,LottieAnimationView.setCompostion(Compostion) 最终落实到LottieDrawable.setCompostion(Compostion),LottieDrawable 为了获取动画里面的 bitmap 对象,Lottie 框架封装了ImageAssetBitmapManager 对象,在 LottieDrawable 中创建,将图片的获取转移到 imageAssetBitmapManager 中,并暴露 public Bitmap bitmapForId(String id) 的方法。
LottieImageAsset imageAsset = imageAssets.get(id);
上面的 bitmapForId(String id) 方法体中有这么一行代码,如上,之前Json 动画文件解析的图片都存放到 imageAssets中,id 是当前需要加载的图片素材名,通过 get 获取到对应的 LottieImageAsset 对象,其实里面也就包装了该 id 值,做这层包装可能为了以后方便扩展吧!
if (assetDelegate != null) {
bitmap = assetDelegate.fetchBitmap(imageAsset);
bitmaps.put(id, bitmap);
return bitmap;
}
...
is = context.getAssets().open(imagesFolder + imageAsset.getFileName());
bitmap = BitmapFactory.decodeStream(is, null, opts);
return bitmap;
...
同样从 bitmapForId(String id) 方法体中提取出如上代码,从上面可以看出如果 assetDelegate == null,它就会从 Asset的imagesFolder 目录下找素材文件。因为之前我们并没有设置过 assetDelegate,而且我们的素材并不是在 Asset 的 imagesFolder 目录下,所以获取不到 bitmap 对象,动画无法播放也是情有可原的,不断的反向追溯assetDelegate 来源,找到LottieAnimationView.setImageAssetDelegate(ImageAssetDelegate assetDelegate) 方法,所以调整之前的代码,如下:
public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
String filePath = currentImgFolder + File.separator + asset.getFileName();
return BitmapFactory.decodeFile(filePath, opts);
}
}
public static void playAnimation(LottieAnimationView animationView, String imageFolder, ImageAssetDelegate imageAssetDelegate, LottieComposition composition) {
if (animationView == null || composition == null) {
return;
}
animationView.setComposition(composition);
animationView.setImageAssetsFolder(imageFolder);
animationView.setImageAssetDelegate(imageAssetDelegate);
animationView.playAnimation();
}
到现在为此,这个动画才能播放出来,这个地方有一点比较坑的就是ImageAssetDelegate 的创建:
public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
String filePath = currentImgFolder + File.separator + asset.getFileName();
return BitmapFactory.decodeFile(filePath, opts);
}
}
每次使用的时候,我们都需要有这样一个 currentImgFolder 变量,维护这个文件所在的父目录的位置,其实框架大可以在ImageAssetBitmapManager 中这样调用,将之前我们用setImageFolder(String folder) 又重新的回调回来。
if (assetDelegate != null) {
bitmap = assetDelegate.fetchBitmap(imagesFolder, imageAsset); // imagesFolder是新加
bitmaps.put(id, bitmap);
return bitmap;
}
动画展示效果不正常
在动画 json 文件中,有如下类似的数据,其中 W 和 H 字段声明了整个动画的输出大小,你需要确保你使用的LottieAnimationVIew 的宽高比和这个一致。
{"v":"4.9.0","fr":25,"ip":0,"op":50,"w":1242,"h":128,"nm":"WWW","ddd":0,"assets": ....
播放本地动画文件展示的动画偏小或偏大
注意 ImageAssetDelegate 的 fetBitmap() 代码中 indensity 属性的设置
@Override
public Bitmap fetchBitmap(LottieImageAsset asset) {
String filePath = currentImgFolder + File.separator + asset.getFileName();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inDensity = 110; //请留意这个值的设定
return BitmapFactory.decodeFile(filePath, opts);
}
同一个 LottieAnimationView 播放两个带素材的 Json 动画文件,会共用同一张素材图片。
Bitmap bitmapForId(String id) {
Bitmap bitmap = bitmaps.get(id);
...
return bitmap;
}
在 ImageAssetBitmapManager 的 bitmapForId() 的方法会对bitmap 进行缓存,建立<文件名 : Bitmap>的映射表,如果两个Json 动画文件里面使用的素材图同名的话,就会出现如下问题。解决办法就是保证每个 Json 动画文件使用的素材图片不同名。
实用总结
播放放置在 Asset 目录下的动画文件
设置播放文件: setAnimation("文件名")
如果动画文件带素材: setImageAssetsFolder("文件夹名")
播放系统目录下的动画文件
异步获取 Compostion 对象: LottieComposition.Factory.fromInputStream()
设置播放的素材: setComposition(composition)
如果动画文件带素材: setImageAssetsFolder("文件夹名") + setImageAssetDelegate(imageAssetDelegate)
与之相关