LottieAndroid使用詳解及源碼解析,讓你的應(yīng)用加載動(dòng)畫變得輕而易舉。
看源碼的時(shí)候跟著我最下面的兩張時(shí)序圖慢慢走,一遍看不懂就再看一遍,別著急,多看幾遍就能懂了。雖然這里不是用的最新框架源碼,但是基本原理都是一致的,不影響你使用最新版本時(shí)候遇到問題,然后定位解決。
看懂源碼有什么用?當(dāng)你使用這個(gè)框架遇到一些奇怪的Bug的時(shí)候,有時(shí)候不一定是你的問題,可能是源碼中的問題,追蹤源碼有利于你快速定位問題。我在使用的時(shí)候遇到過源碼的問題,后來在GitHub的issue中看到有人提了,再后來的版本就修復(fù)了。
為了寫好這篇文章,我花了一周多的時(shí)間,因?yàn)槭墙o公司做分享,所以很多細(xì)節(jié)我是講出來的,并沒有寫出來,因?yàn)橐獙懙脑挄?huì)很多,但這篇文章基本上完全可以滿足你對(duì)Lottie的使用和對(duì)源碼的理解,接下來耐心看吧。
我們主要從以下四個(gè)方面來講解:
一、Lottie簡(jiǎn)介
二、LottieAndroid的使用
三、LottieAndroid源碼解析
四、可能遇到的問題會(huì)有哪些
一、Lottie簡(jiǎn)介
-
Lottie是什么?
Lottie是Airbnb開源的一個(gè)動(dòng)畫渲染庫(kù),同時(shí)支持Android、IOS、React Native和Web平臺(tái),Lottie目前只支持渲染播放After Effects動(dòng)畫。Lottie使用bobymovin(After Effects插件)到處的json數(shù)據(jù)作為動(dòng)畫數(shù)據(jù)源。使用Lottie可以讓動(dòng)畫顯示變得簡(jiǎn)單方便。
-
從動(dòng)畫制作到動(dòng)畫顯示流程如下:
-
工作流程:
- 設(shè)計(jì)師使用Affer Effects制作動(dòng)畫并導(dǎo)出json文件,參考:Lottie動(dòng)畫社區(qū)
- 各端開發(fā)使用相應(yīng)的LottieSDK實(shí)現(xiàn)動(dòng)畫效果,GitHub下載地址:Lottie Android,Lottie IOS,React Native,Web
-
注意事項(xiàng):
設(shè)計(jì)師同學(xué)制作各個(gè)平臺(tái)(Android、IOS、React Netive)動(dòng)畫時(shí)需要查看Lottie在不同平臺(tái)支持的特性,否則制作出來的動(dòng)畫顯示可能會(huì)有問題,設(shè)計(jì)同學(xué)制作動(dòng)畫參考:不同平臺(tái)Lottie支持特性
-
為什么要使用Lottie?
-
先看看在沒有Lottie之前我們是怎么實(shí)現(xiàn)相對(duì)復(fù)雜動(dòng)畫的:
- 使用GIF,占用空間大,有些動(dòng)畫顯示效果不佳,需要適配分辨率,Android原生不支持GIF動(dòng)畫的顯示。
- 使用幀動(dòng)畫,占用空間大,依然會(huì)遇到不同分辨率適配的問題。
- 組合式動(dòng)畫,通過大量代碼實(shí)現(xiàn)復(fù)雜的動(dòng)畫效果。
-
使用Lottie可以解決的問題:
- 降低動(dòng)畫設(shè)計(jì)和開發(fā)成本
- 解決設(shè)計(jì)提供動(dòng)畫效果與實(shí)現(xiàn)不一致問題
- 占用空間更小
- 不同的手機(jī)分辨率不需要適配
-
-
Lottie適用于哪些場(chǎng)景?
- 啟動(dòng)(splash)動(dòng)畫:典型場(chǎng)景是APP logo動(dòng)畫的播放
- 上下拉刷新(refresh)動(dòng)畫:所有APP都必備的功能,利用 Lottie 可以做的更加簡(jiǎn)單酷炫了
- 加載(loading)動(dòng)畫:典型場(chǎng)景是網(wǎng)絡(luò)請(qǐng)求的loading動(dòng)畫
- 提示(tips)動(dòng)畫:典型場(chǎng)景是空白頁的提示
- 按鈕(button)動(dòng)畫:典型場(chǎng)景如switch按鈕、編輯按鈕、播放按鈕等按鈕的
- 禮物(gift)動(dòng)畫:典型場(chǎng)景是直播類APP的高級(jí)動(dòng)畫播放
-
我們想要使用Lottie替代哪些動(dòng)畫?
- 首先并不是在APP中所有的動(dòng)畫都要用Lottie來替換
- 一些可以通過屬性動(dòng)畫來實(shí)現(xiàn)的簡(jiǎn)單動(dòng)畫就不需要用Lottie來實(shí)現(xiàn)了
- 替代一些通過代碼不好實(shí)現(xiàn)的動(dòng)畫效果
- 替代GIF動(dòng)畫和幀動(dòng)畫
二、LottieAndroid的使用
-
集成到項(xiàng)目中(以2.2.0版本為例)
-
添加依賴:compile 'com.airbnb.android:lottie:2.2.0'
Lottie版本號(hào)參考:Maven庫(kù)查看Lottie各版本號(hào)
-
Gradle依賴修改:
最低版本:MIN_SDK_VERSION = 16
編譯版本:COMPILE_SDK_VERSION = 25
所有的兼容包需要升級(jí)到版本號(hào)為25.3.1compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support:design:25.3.1' compile 'com.android.support:recyclerview-v7:25.3.1' compile 'com.android.support:palette-v7:25.3.1' compile 'com.android.support:support-v4:25.3.1'
-
如何查看Lottie各版本需要對(duì)應(yīng)的AndroidSDK編譯版本和兼容包版本號(hào)?
以Lottie2.2.0版本為例:mvnrepository lottie2.2.0
-
-
使用方法
-
Lottie基本用法查看官方文檔
-
首先將json文件放到assets文件夾下:
可以直接放到assets目錄下,或者在assets目錄下創(chuàng)建一個(gè)二級(jí)目錄放在二級(jí)目錄下。 -
在布局中添加LottieAnimationView控件:
LottieAnimationView可以設(shè)置的屬性如下:<com.airbnb.lottie.LottieAnimationView android:id="@+id/animation_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_fileName="hello-world.json" app:lottie_loop="true" app:lottie_autoPlay="true" /> 如上圖,如果json文件在assets子文件夾中,lottie_fileName="lottieani/stars.json"
-
得到LottieAnimationView對(duì)象進(jìn)行動(dòng)畫操作:
setAnimation()有三種方法,可以直接設(shè)置動(dòng)畫的Json對(duì)象,或者設(shè)置Json文件相對(duì)路徑名,且支持設(shè)置緩存類型:LottieAnimationView animationView = (LottieAnimationView)findViewById(R.id.animation_view); // 布局中不指定文件可以在此設(shè)置,路徑設(shè)置同布局文件 animationView.setAnimation("hello-world.json"); // 是否循環(huán)播放 animationView.loop(true); // 設(shè)置播放速率,例如:2代表播放速率是不設(shè)置時(shí)的二倍 animationView.setSpeed(2f); // 開始播放 animationView.playAnimation(); // 暫停播放 animationView.pauseAnimation(); // 取消播放 animationVIew.cancelAnimation(); // 設(shè)置播放進(jìn)度 animationView.setProgress(0.5f); // 判斷是否正在播放 animationView.isAnimating();
-
添加動(dòng)畫監(jiān)聽
mAnimationView.addAnimatorListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { Log.d(TAG, "onAnimationStart : " + animation.getDuration()); } @Override public void onAnimationEnd(Animator animation) { Log.d(TAG, "onAnimationEnd"); } @Override public void onAnimationCancel(Animator animation) { Log.d(TAG, "onAnimationCancel"); } @Override public void onAnimationRepeat(Animator animation) { Log.d(TAG, "onAnimationRepeat"); } }); mAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //Log.d(TAG, "onAnimationUpdate : " + animation.getCurrentPlayTime()); } });
-
-
其他用法
-
自定義動(dòng)畫的速率和時(shí)長(zhǎng)
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();
-
給整個(gè)動(dòng)畫添加一個(gè)特定圖層,或者一個(gè)圖層的特定內(nèi)容添加一個(gè)顏色過濾器
// 任何符合顏色過濾界面的類 final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.LIGHTEN); // 在整個(gè)視圖中添加一個(gè)顏色過濾器 animationView.addColorFilter(colorFilter); //在特定的圖層中添加一個(gè)顏色濾鏡 animationView.addColorFilterToLayer("hello_layer", colorFilter); // 添加一個(gè)彩色過濾器特效“hello_layer”上的內(nèi)容 animationView.addColorFilterToContent("hello_layer", "hello", colorFilter); // 清除所有的顏色濾鏡 animationView.clearColorFilters();
-
在列表中添加動(dòng)畫,每個(gè)item循環(huán)播放該動(dòng)畫
如果按照下面寫法會(huì)有什么問題?如果按照下面寫法,即便是已經(jīng)設(shè)置了lottieViewLive.loop(true)循環(huán)播放,item被劃走再劃回來動(dòng)畫依舊會(huì)停止播放。
<com.airbnb.lottie.LottieAnimationView android:id="@+id/lottieViewLive" android:layout_width="30dp" android:layout_height="30dp"/> // 在viewHolder中initView的時(shí)候開始動(dòng)畫 LottieAnimationView lottieViewLive = (LottieAnimationView) layout.findViewById(R.id.lottieViewLive); lottieViewLive.setAnimation("lottiejson/living.json"); // 循環(huán)播放動(dòng)畫 lottieViewLive.loop(true); lottieViewLive.playAnimation();
為了解決上訴問題,只需要在布局中添加,屬性app:lottie_autoPlay="true";可以直接按照寫法在xml布局中添加動(dòng)畫即可。為什么會(huì)這樣?待會(huì)在源碼解析部分講解。
<com.airbnb.lottie.LottieAnimationView android:id="@+id/lottieViewLive" android:layout_width="30dp" android:layout_height="30dp" app:lottie_autoPlay="true" app:lottie_fileName="lottiejson/living.json" app:lottie_loop="true"/>
-
從網(wǎng)絡(luò)直接獲取json數(shù)據(jù)顯示動(dòng)畫(Lottie本身不提供網(wǎng)絡(luò)請(qǐng)求)
為什么要使用下面方式,而不是從網(wǎng)絡(luò)獲取json數(shù)據(jù)后直接animationView.setAnimation(json)。主要是為了使用OnCompositionLoadedListener,防止從網(wǎng)絡(luò)獲取的json數(shù)據(jù)錯(cuò)誤解析出來的composition為null。
LottieComposition.Factory.fromJson(getResources(), jsonObject, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(@Nullable LottieComposition composition) { if (composition != null) { mAnimationView.setComposition(composition); mAnimationView.playAnimation(); } else { // showErrorView(); } } });
-
加載含有圖片的復(fù)雜文件
有些動(dòng)畫比較復(fù)雜不是簡(jiǎn)單的線條、圖塊所能實(shí)現(xiàn)的,這樣的動(dòng)畫通過AE插件bobymovin生成的文件除了有一個(gè)json文件外,還會(huì)有一些png圖片。如何加載這樣的動(dòng)畫呢?
① 加載本地含有圖片的動(dòng)畫資源,json文件放在assets文件夾下,生成的png圖片同樣放在assets文件夾下(或者自己在assets文件夾下創(chuàng)建一個(gè)子目錄,假如叫l(wèi)ottieimage)。
② 加載從網(wǎng)絡(luò)獲取的含有圖片的動(dòng)畫資源,首先需要將資源下載到手機(jī)SD卡中// 代碼這樣寫,需要設(shè)置圖片的文件夾地址 mAnimationView.setImageAssetsFolder("lottieweaccept/images"); mAnimationView.setAnimation("lottieweaccept/WeAccept.json"); mAnimationView.playAnimation(); // 或者在布局文件中添加這句代碼,指定圖片路徑 app:lottie_imageAssetsFolder="lottieweaccept/images"
final String absolutePath = imagesDir.getAbsolutePath(); mAnimationView.setImageAssetDelegate(new ImageAssetDelegate() { @Override public Bitmap fetchBitmap(LottieImageAsset asset) { return BitmapFactory.decodeFile(absolutePath + File.separator + asset.getFileName()); } }); LottieComposition.Factory.fromInputStream(LottieViewActivity.this, inputStream, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(@Nullable LottieComposition composition) { if (composition != null) { mAnimationView.setComposition(composition); mAnimationView.playAnimation(); } else { // doOtherThing(); } } });
-
-
三、源碼解析
-
從Json到動(dòng)畫顯示的實(shí)現(xiàn)思路
將復(fù)雜的圖片使用圖層表示,每一層表示不同的內(nèi)容
-
根據(jù)動(dòng)畫需求可以只針對(duì)某一層做相應(yīng)的平移、旋轉(zhuǎn)、縮放等動(dòng)畫
-
Json文件中數(shù)據(jù)轉(zhuǎn)成LottieComposition數(shù)據(jù)對(duì)象,LottieDrawable負(fù)責(zé)將數(shù)據(jù)繪制成drawable,LottieAnimationView負(fù)責(zé)將LottieDrawable顯示出來。LottieAnimationView繼承自AppCompatImageView,LottieDrawable繼承自Drawable。
-
先看看生成的Json數(shù)據(jù),參考這里:Lottie:讓動(dòng)畫如此簡(jiǎn)單
-
如何加載json數(shù)據(jù)并顯示圖像的?
-
animationView.setAnimation("hello-world.json");
通過setAnimation()來看看,上面json數(shù)據(jù)到顯示圖像的過程。源碼時(shí)序圖如下(以2.2.0版本為例):LottieAnimationView初始化的時(shí)候會(huì)首先創(chuàng)建LottieDrawable對(duì)象,private final LottieDrawable lottieDrawable = new LottieDrawable()。init(),進(jìn)行初始化的時(shí)候,解析xml設(shè)置的屬性。
setAnimation(String fileName),加載Json文件。
通過異步加載,最終會(huì)調(diào)用到fromJsonSync()對(duì)Json文件進(jìn)行解析。
解析結(jié)果通過onCompositionLoaded()回調(diào)到主線程,然后會(huì)將Json數(shù)據(jù)轉(zhuǎn)換成LottieComposition對(duì)象。
lottieDrawable.setComposition(),將LottieComposition對(duì)象設(shè)置給LottieDrawable。
生成CompositionLayer對(duì)象,這個(gè)對(duì)象就是每一個(gè)圖層的對(duì)象。
通過setImageDrawable(lottieDrawable)將圖像顯示出來,顯示第一幀動(dòng)畫。
-
-
動(dòng)畫如何運(yùn)行起來的?
-
animationView.playAnimation();
調(diào)用playAnimation()動(dòng)畫是如何動(dòng)起來的,源碼時(shí)序圖如下(以2.2.0版本為例):ValueAnimator實(shí)現(xiàn)的控制動(dòng)畫進(jìn)度。
setProgress實(shí)現(xiàn)的顯示具體進(jìn)度動(dòng)畫。
LottieDrawable中的draw繪制圖像,Canvas進(jìn)行繪制。
-
-
為什么在列表中加載動(dòng)畫時(shí)設(shè)置了循環(huán)播放,item劃走再劃回來動(dòng)畫還是會(huì)停止播放?為什么添加app:lottie_autoPlay="true"才行?
-
我們來看看這兩句代碼,有沒有什么問題?
setAnimation是異步加載json文件的,調(diào)用setAnimation之后直接調(diào)用playAnimation()源碼是如何保證加載完數(shù)據(jù)之后再開始動(dòng)畫呢?最終會(huì)走到LottieDrawable類中,如下:// 異步加載json文件 animationView.setAnimation("hello-world.json"); animationView.playAnimation(); animationView.setSpeed(2f);
性能問題
-
性能和內(nèi)存
- 如果沒有mask和mattes,那么性能和內(nèi)存非常好,沒有bitmap創(chuàng)建,大部分操作都是簡(jiǎn)單的cavas繪制。
- 如果存在mattes,將會(huì)創(chuàng)建2~3個(gè)bitmap。bitmap在動(dòng)畫加載到window時(shí)被創(chuàng)建,被window刪除時(shí)回收。所以不宜在RecyclerView中使用包涵mattes或者mask的動(dòng)畫,否則會(huì)引起bitmap抖動(dòng)。除了內(nèi)存抖動(dòng),mattes和mask中必要的bitmap.eraseColor()和canvas.drawBitmap()也會(huì)降低動(dòng)畫性能。對(duì)于簡(jiǎn)單的動(dòng)畫,在實(shí)際使用時(shí)性能不太明顯。
- 如果在列表中使用動(dòng)畫,推薦使用緩存LottieAnimationView.setAnimation(String, CacheStrategy) 。
全屏適配參考
更新:添加2.5.1版本時(shí)遇到的問題
1、 編譯版本需要 27
2、所有的suport包版本需要27.1.0
compile 'com.android.support:appcompat-v7:27.1.0'
3、如果有引入lifecycle,版本需要1.1.0
android.arch.lifecycle:extensions:1.1.0
android.arch.lifecycle:compiler:1.1.0
如果項(xiàng)目中引入了livedata并且lifecycle版本號(hào)不對(duì),會(huì)報(bào)如下錯(cuò)誤:
Error:Program type already present: android.arch.lifecycle.LiveData
參考:https://stackoverflow.com/questions/49056723/errorprogram-type-already-present-android-arch-lifecycle-livedata
com.firebaseui:firebase-ui-firestore:3.1.0 depends on android.arch.lifecycle:extensions:1.0.0-beta1. Switching to version 3.2.2 fixes the issue by using the Lifecycle 1.1 libraries that Support Library 27.1.0 are built upon.