lottie的使用通读一遍官方文档基本就可以拿来用了,也可以看看我之前的Lottie使用;但今天要说的是另外一种情况,就是通过网络拿到动画资源的zip包,再来加载动画:
使用场景就是,zip包中含有动画的json文件以及动画需要的素材文件;
首先估计下会遇到的问题可能有如下:
- json文件和素材文件都放在Asset目录下,LottieAnimationView可以直接加载,那么如何加载指定目录的资源(为解决加载网络资源).
- 通过网络拿到的动画zip包怎么拆分出动画json文件和素材文件
解决第一个问题,我好奇的点进了setAnimation这个方法,想知道LottieAnimationView是如何把指定json文件转为动画实现的,发现它只是调用了同名方法setAnimation(animationName, defaultCacheStrategy)
通过参数,可以看出第二个参数是缓存策略;接着往下看:
/**
* Sets the animation from a file in the assets directory.
* This will load and deserialize the file asynchronously.
* <p>
* You may also specify a cache strategy. Specifying {@link CacheStrategy#Strong} will hold a
* strong reference to the composition once it is loaded
* and deserialized. {@link CacheStrategy#Weak} will hold a weak reference to said composition.
*/
public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
this.animationName = animationName;
animationResId = 0;
if (ASSET_WEAK_REF_CACHE.containsKey(animationName)) {
WeakReference<LottieComposition> compRef = ASSET_WEAK_REF_CACHE.get(animationName);
LottieComposition ref = compRef.get();
if (ref != null) {
setComposition(ref);
return;
}
} else if (ASSET_STRONG_REF_CACHE.containsKey(animationName)) {
setComposition(ASSET_STRONG_REF_CACHE.get(animationName));
return;
}
clearComposition();
cancelLoaderTask();
compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName,
new OnCompositionLoadedListener() {
@Override public void onCompositionLoaded(LottieComposition composition) {
if (cacheStrategy == CacheStrategy.Strong) {
ASSET_STRONG_REF_CACHE.put(animationName, composition);
} else if (cacheStrategy == CacheStrategy.Weak) {
ASSET_WEAK_REF_CACHE.put(animationName, new WeakReference<>(composition));
}
setComposition(composition);
}
});
}
可以看到缓存策略相关包括强引用缓存,弱引用缓存,和无缓存模式,Json动画文件最终会通过LottieComposition.Factory.fromAssetFileName异步转化为Composition对象;继续跟进这个方法:
/**
* Loads a composition from a file stored in /assets.
*/
public static Cancellable fromAssetFileName(
Context context, String fileName, OnCompositionLoadedListener listener) {
InputStream stream;
try {
stream = context.getAssets().open(fileName);
} catch (IOException e) {
throw new IllegalArgumentException("Unable to find file " + fileName, e);
}
return fromInputStream(stream, listener);
}
通过context.getAssets().open(fileName);将传入的文件以流的方式处理,跟到这里,估计就知道如何加载其他目录的文件。
通过流的方式拿到指定目录,并通过LottieComposition.Factory.fromAssetFileName创建Composition对象,然后再set给LottieAnimationView;
在加载包含素材文件的动画文件的时候,需要注意一点,稍不注意,就一定会遇到下面这个错误:
You must set an images folder before loading an image. Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder
怎么解决尼?得看你是在那种场景遇到的这个问题,
- 如果是本地加载,且动画文件包和素材文件都在assets下的时候,只需要在调用playAnimation之前调用setImageAssetsFolder("你的素材文件名")
- 本地加载,动画文件和素材文件不在assets下的时候,那么就有点复杂了;
在第二种情况的时候,跟进setImageAssetsFolder方法;
public void setImageAssetsFolder(String imageAssetsFolder) {
lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);
}
继续,
public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
this.imageAssetsFolder = imageAssetsFolder;
}
将其赋值给了imageAssetsFolder,查看imageAssetsFolder的引用处,
private ImageAssetManager getImageAssetManager() {
if (getCallback() == null) {
// We can't get a bitmap since we can't get a Context from the callback.
return null;
}
if (imageAssetManager != null && !imageAssetManager.hasSameContext(getContext())) {
imageAssetManager.recycleBitmaps();
imageAssetManager = null;
}
if (imageAssetManager == null) {
imageAssetManager = new ImageAssetManager(getCallback(),
imageAssetsFolder, imageAssetDelegate, composition.getImages());
}
return imageAssetManager;
}
被当做参数传入了ImageAssetManager,跟进这个类,继续查找,最终发现被用到的地方
@Nullable public Bitmap bitmapForId(String id) {
Bitmap bitmap = bitmaps.get(id);
if (bitmap != null) {
return bitmap;
}
LottieImageAsset imageAsset = imageAssets.get(id);
if (imageAsset == null) {
return null;
}
if (delegate != null) {
bitmap = delegate.fetchBitmap(imageAsset);
if (bitmap != null) {
putBitmap(id, bitmap);
}
return bitmap;
}
String filename = imageAsset.getFileName();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = true;
opts.inDensity = 160;
if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
// Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
byte[] data;
try {
data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.w(L.TAG, "data URL did not have correct base64 format.", e);
return null;
}
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts);
return putBitmap(id, 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 + filename);
} catch (IOException e) {
Log.w(L.TAG, "Unable to open asset.", e);
return null;
}
bitmap = BitmapFactory.decodeStream(is, null, opts);
return putBitmap(id, 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)拿到之前的assets,Json动画文件解析的图片都存放到imageAssets中,id是当前需要加载的图片素材名,通过get获取到对应的LottieImageAsset对象;
通过
if (delegate != null) {
bitmap = delegate.fetchBitmap(imageAsset);
if (bitmap != null) {
putBitmap(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 + filename);
} catch (IOException e) {
Log.w(L.TAG, "Unable to open asset.", e);
return null;
}
可以看出,当delegate == null的情况下,它会从Asset的imagesFolder目录下找素材文件。但是我们如果没有设置assetDelegate,而我们加载的动画json为其他目录,素材也并不是在Asset的imagesFolder目录下,所以就获取不到bitmap对象,也就会看到上面哪个错误
throw new IllegalStateException("You must set an images folder before loading an image." +" Set it with LottieComposition#setImagesFolder or LottieDrawable#setImagesFolder")**;
现在我们就需要知道ImageAssetDelegate是个什么东西了,发现在LottieDrawable中有这样一个方法
public void setImageAssetDelegate(
@SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
this.imageAssetDelegate = assetDelegate;
if (imageAssetManager != null) {
imageAssetManager.setDelegate(assetDelegate);
}
}
继续查看引用,会在熟悉的LottieAnimationView中发现
public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
lottieDrawable.setImageAssetDelegate(assetDelegate);
}
我们可以看到ImageAssetDelegate是一个接口
public interface ImageAssetDelegate {
Bitmap fetchBitmap(LottieImageAsset asset);
}
因此可以在调用setImageAssetsFolder的时候,调用setImageAssetDelegate,传入实现的ImageAssetDelegate,即可解决上面的第二种情况遇到的问题;
加载网络zip包的动画文件方式
最简单的是不含素材文件的zip包:
其次是包含素材文件的zip包:
发表点一些可能遇到的问题:
- 空指针问题:
@Nullable public Bitmap bitmapForId(String id) {
Bitmap bitmap = bitmaps.get(id);
if (bitmap != null) {
return bitmap;
}
LottieImageAsset imageAsset = imageAssets.get(id);
if (imageAsset == null) {
return null;
}
if (delegate != null) {
bitmap = delegate.fetchBitmap(imageAsset);
if (bitmap != null) {
putBitmap(id, bitmap);
}
return bitmap;
}
String filename = imageAsset.getFileName();
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inScaled = true;
opts.inDensity = 160;
if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
// Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
byte[] data;
try {
data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.w(L.TAG, "data URL did not have correct base64 format.", e);
return null;
}
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, opts);
return putBitmap(id, 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 + filename);
} catch (IOException e) {
Log.w(L.TAG, "Unable to open asset.", e);
return null;
}
bitmap = BitmapFactory.decodeStream(is, null, opts);
return putBitmap(id, bitmap);
}
public void recycleBitmaps() {
synchronized (bitmapHashLock) {
Iterator<Map.Entry<String, Bitmap>> it = bitmaps.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Bitmap> entry = it.next();
entry.getValue().recycle();
it.remove();
}
}
}
在回收的时候,就会爆发;我们可以在加载的时候自己给兜住;
- 动画View的设置,在LottieAnimationView设置宽高的时候应该遵循json文件中的宽高比;
- 加载自己指定的文件动画时候,可能出现动画偏大或者偏小
可以在设置
通过设置ImageAssetDelegate的时候
@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);
}
改变opts.inDensity的值;