Android•Lottie动画库填坑记
1. 入坑背景
由于從事直播軟件開發(fā)的緣故,本猿在版本迭代過程中一期不落的接觸到各式各樣動畫效果。最早的時候,苦逼的用Android原生動畫做直播間全屏禮物,反復的看著美工給的Flash效果圖,不斷的拼湊素材圖片,調(diào)整控制動畫播放的屬性值,各個動畫代碼都很類似,但卻無法套用,一連兩三天下來,基本上腦海中除了動畫就一片空白...不過后來采用spine禮物框架以后,也就告別這樣的悲慘人生。然而就在上一版本中,產(chǎn)品因為...的原因,讓不同的用戶進入房間有不一樣的效果,其中就包括文字背景帶粒子效果,對于這樣的效果,Android原生動畫顯然無能為力,如果采用幀動畫,由于大量素材文件的引入帶來最直接的不良影響就是安裝包體積過大。經(jīng)過評估之后,決定使用三方動畫框架,從服務器下載動畫資源,在特定時間對不同資源文件進行播放,最終采用相對比較成熟的Lottie框架。
2. 踩坑準備
熟悉一個新的框架最快的方式就是查看官方文檔,因為官方文檔中一般都會給出一個Demo,果不其然,Lottie也是!文檔的閱讀量不是很大,通篇下來介紹了:
- 播放本地Assets目錄下的Json動畫文件
- 通過Json數(shù)據(jù)播放動畫
- 如何對動畫進行監(jiān)聽以及動畫進度調(diào)節(jié)
- Lottie動畫數(shù)據(jù)的預加載和緩存
- 為Assets目錄下的Json動畫文件配置動畫所需要的素材
3. 開始入坑
然而,他介紹了這么多,并沒有一款適合我的。因為服務器下發(fā)不是簡單的Json數(shù)據(jù),是一個動畫壓縮包,里面包括了動畫文件和播放動畫需要的素材文件,而且解壓后的文件也不在Asset目錄下。于是,只好跟蹤animationView.setAnimation("hello-world.json")源碼,看看最終到底做了什么事!
public void setAnimation(String animationName) {setAnimation(animationName, defaultCacheStrategy);} 復制代碼一個參數(shù)調(diào)用兩個參數(shù)同名方法,只好接著往下看!
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() {@Overridepublic 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.Factory.fromAssetFileName(...)的方法異步獲取的,于是我們只好接著往下跟蹤。
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() {@Overridepublic 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() {@Overridepublic 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<>(); //緩存Compositionpublic 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)處理,然后調(diào)用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 復制代碼看到這個錯誤,想起官方文檔上面有說,如何為動畫配置播放動畫所需要的素材,而且錯誤提示也特別的明顯,看了看給的資源包的目錄,似乎發(fā)現(xiàn)了什么!于是我按照官方《為Assets目錄下的Json動畫文件設置播放動畫所需要的資源》一樣,改了一下代碼:
public static void playAnimation(LottieAnimationView animationView,String imageFolder, LottieComposition composition) {animationView.setComposition(composition);animationView.setImageAssetsFolder(imageFolder); // 新添加的animationView.playAnimation();} 復制代碼想著異常信息都提示這么明顯了,而且官方文檔給的模板也是這樣寫的,我更加確定這次動畫播放絕對的沒有問題。然而,動畫最終還是沒有播放出來!沒辦法,只好繼續(xù)翻源碼,既然Assets目錄下setImageAssetsFolder(String folder)能生效,那我們只好從這個方法切入,看看folder變量最終是怎么樣被使用的。
@SuppressWarnings("WeakerAccess") public void setImageAssetsFolder(String imageAssetsFolder) {lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);} 復制代碼沒有什么頭緒只好繼續(xù)往下看:
@SuppressWarnings("WeakerAccess") public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {this.imageAssetsFolder = imageAssetsFolder;} 復制代碼這個變量被設置成類屬性了,那么我們只需要在這個類下搜索怎么樣被使用就可以馬上定位出原因,發(fā)現(xiàn)有這么一行:
imageAssetBitmapManager = new ImageAssetBitmapManager(getCallback(),imageAssetsFolder, imageAssetDelegate, composition.getImages());} 復制代碼我擦,變量被傳遞到一個ImageAssetBitmapManager對象里面去了,只好進這個類繼續(xù)跟蹤,最終定位到這樣一個方法:
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數(shù)據(jù)必然會聲明該圖片文件名。在Composition.Factory進行解析為Composition時,里面使用的圖片都以鍵值對的方式存放到Composition的 private final Map<String, LottieImageAsset> images = new HashMap<>()中,LottieAnimationView.setCompostion(Compostion)最終落實到LottieDrawable.setCompostion(Compostion),LottieDrawable為了獲取動畫里面的bitmap對象,Lottie框架封裝了ImageAssetBitmapManager對象,在LottieDrawable中創(chuàng)建,將圖片的獲取轉移到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)方法,所以調(diào)整之前的代碼,如下:
public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {@Overridepublic 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();} 復制代碼到現(xiàn)在為此,這個動畫才能播放出來,這個地方有一點比較坑的就是ImageAssetDelegate的創(chuàng)建:
public static ImageAssetDelegate imageAssetDelegate = new ImageAssetDelegate() {@Overridepublic Bitmap fetchBitmap(LottieImageAsset asset) {String filePath = currentImgFolder + File.separator + asset.getFileName();return BitmapFactory.decodeFile(filePath, opts);}} 復制代碼每次使用的時候,我們都需要有這樣一個currentImgFolder 變量,維護這個文件所在的父目錄的位置,其實框架大可以在ImageAssetBitmapManager中這樣調(diào)用,將之前我們用setImageFolder(String folder)又重新的回調(diào)回來。
if (assetDelegate != null) {bitmap = assetDelegate.fetchBitmap(imagesFolder, imageAsset); // imagesFolder是新加bitmaps.put(id, bitmap);return bitmap;} 復制代碼4. Lottie坑點總結
- 在動畫json文件中,有如下類似的數(shù)據(jù),其中W 和 H字段聲明了整個動畫的輸出大小,你需要確保你使用的LottieAnimationVIew的寬高比和這個一致。
- 播放本地動畫文件展示的動畫偏小或偏大
注意ImageAssetDelegate的fetBitmap()代碼中indensity屬性的設置
@Overridepublic 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); //這里還有坑,請往下接著看} 復制代碼- Lottie庫回收素材圖片bitmap引發(fā)的空指針問題 (1) 先看看Lottie對素材圖片進行緩存的方法:
(2) 再看看Lottie對緩存圖片的回收處理:
void recycleBitmaps() {Iterator<Map.Entry<String, Bitmap>> it = bitmaps.entrySet().iterator();while (it.hasNext()) {Map.Entry<String, Bitmap> entry = it.next();entry.getValue().recycle();it.remove();}} 復制代碼(3) 結論: 前后對比,有沒有發(fā)現(xiàn)Lottie對緩存的素材圖片bitmap對象并沒有做判空處理,就直接回收了(Version 1.5.3)。
解決辦法: 如果是加載本地素材圖片(非Assets目錄)可以采用如下辦法:
public Bitmap fetchBitmap(LottieImageAsset asset) {String filePath = currentImgFolder + File.separator + asset.getFileName();Bitmap bitmap = BitmapFactory.decodeFile(filePath, opts);if (bitmap == null) {bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);}return bitmap;} 復制代碼5. 使用總結
- 播放放置在Asset目錄下的動畫文件
設置播放文件: setAnimation("文件名") 如果動畫文件帶素材: setImageAssetsFolder("文件夾名")
- 播放系統(tǒng)目錄下的動畫文件
異步獲取Compostion對象: LottieComposition.Factory.fromInputStream() 設置播放的素材: setComposition(composition) 如果動畫文件帶素材: setImageAssetsFolder("文件夾名") + setImageAssetDelegate(imageAssetDelegate)
轉載于:https://juejin.im/post/5b8a2fc1e51d4559a81eec56
總結
以上是生活随笔為你收集整理的Android•Lottie动画库填坑记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。