Lottie for Android:一個解放開發者雙手的動畫神器

Lottie
前言
動畫是我們日常開發中必不可少的一個要點,比如某個場景要實現打勾的效果,一開始你的設想可能是這樣子的:

用程式碼實現起來可能還好,繪製圓圈,一段圓弧圓周運動,繪製一個打勾路徑,結合動畫實現,美滋滋。但是等到設計師出圖的那一天,是這樣的:

看了效果圖,內心有一句話不知道當不當講

但是不要慌,你能不能想到竟然有這麼一個庫,可以讓設計師"幫你實現動畫效果"!沒錯,它就是今天的主角——Lottie。Lottie 是Airbnb開源的一個面向 iOS、Android、React Native 的動畫庫,能解析 Adobe After Effects 視訊製作軟體匯出的動畫,並且在程式碼中能通過api直接呼叫這些生成的動畫檔案,完美實現動畫效果,Lottie在這種時候就是救世主級別的存在了。
Lottie官網: https://lottiefiles.com/

lottie效果圖.gif
如何使用
看了效果,我們看下如何在Android中快速使用這個庫實現各種炫酷動畫。
1.匯入Lottie庫
在app的gradle中匯入如下依賴:
dependencies { compile 'com.airbnb.android:lottie:2.2.0' }
- 最新版本可檢視GitHub地址: https://github.com/airbnb/lottie-android
注意:Lottie只支援Api16以上,如果專案中minSdkVersion小於16會報錯。
2.匯入動畫檔案
Lottie載入動畫是靠解析Json檔案來實現的,所以需要設計師從After Effect等軟體中製作好動畫並生成一個Json檔案,檔案的內容大概是下面這個樣子:

json示意圖
儘管看起來很複雜,但這都是通過軟體生成的,我們只需要將其匯入到專案中即可。由於Lottie預設是讀取工程的Assets目錄,所以我們將Json檔案放到src/main/assets目錄下。
3.使用姿勢
1)靜態方式
Lottie庫提供了一個 LottieAnimationView 控制元件,它本質是一個ImageView,可直接在佈局檔案中宣告:
<com.airbnb.lottie.LottieAnimationView android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_fileName="anim.json" app:lottie_loop="true" app:lottie_autoPlay="true"/>
這裡列舉了有幾個lottie的基本屬性
- lottie_fileName 呼叫的Json檔名
- lottie_loop 是否開啟迴圈動畫
- lottie_autoPlay 是否開啟自動播放
當然lottie還有很多其他的屬性,例如:
- lottie_repeatCount 動畫重複次數
- lottie_repeatMode 動畫重複模式
- lottie_scale 放大倍數
這裡就不一一列舉了,執行後效果如下:

lottie demo效果圖.gif
2)動態方式
Lottie也支援在程式碼中動態呼叫介面:
LottieAnimationView lottieAnimationView = findViewById(R.id.anim_view); lottieAnimationView.setAnimation("anim.json"); lottieAnimationView.loop(true); lottieAnimationView.playAnimation();
3)監聽動畫進度
Lottie同樣提供了監聽動畫的介面,而且還是我們熟悉的ValueAnimator
lottieAnimationView.addAnimatorUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { // 判斷動畫載入結束 Log.d("Lottie", "" + valueAnimator.getAnimatedFraction()); } });
4)載入動畫優化
Lottie的效能比屬性動畫略遜一籌,不過支援開啟硬體加速:
lottieAnimationView.useHardwareAcceleration(true);
Lottie支援對動畫的快取,會在動畫載入完成後將其快取為強快取和弱快取
lottieAnimationView.setAnimation("anim.json", LottieAnimationView.CacheStrategy.Strong);//強快取 lottieAnimationView.setAnimation("anim.json", LottieAnimationView.CacheStrategy.Weak);//弱快取
其內部是維護了兩個HashMap:
private static final Map<String, LottieComposition> STRONG_REF_CACHE = new HashMap<>(); private static final Map<String, WeakReference<LottieComposition>> WEAK_REF_CACHE = new HashMap<>(); public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) { //...... if (cacheStrategy == CacheStrategy.Strong) { STRONG_REF_CACHE.put(animationName, composition); } else if (cacheStrategy == CacheStrategy.Weak) { WEAK_REF_CACHE.put(animationName, new WeakReference<>(composition)); } //..... }
原理解析
我們從LottieAnimationView的 playAnimation()
這個方法看下Lottie的動畫流程:
public void playAnimation() { lottieDrawable.playAnimation(); enableOrDisableHardwareLayer(); }
這裡呼叫了一個lottieDrawable的playAnimation(),而lottieDrawable是一個 LottieDrawable
:

LottieDrawable
LottieDrawable
繼承於
Drawable
,Lottie的動畫都是在這個LottieDrawable裡繪製的,它裡面維護了一個
LottieValueAnimator
(繼承於ValueAnimator),我們呼叫的
loop
或者
playAnimation
之類的方法其實本質上都是呼叫了它裡面這個屬性動畫的Api:
public void loop(boolean loop) { animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0); } private void playAnimation(boolean setStartTime) { if (compositionLayer == null) { lazyCompositionTasks.add(new LazyCompositionTask() { @Override public void run(LottieComposition composition) { playAnimation(); } }); return; } long playTime = setStartTime ? (long) (progress * animator.getDuration()) : 0; animator.start(); if (setStartTime) { animator.setCurrentPlayTime(playTime); } }
那麼它是怎麼通過這個屬性動畫去實現繪製的呢?在LottieDrawable的構造方法裡,設定了屬性動畫的監聽器,然後將進度傳給setProgress:
public LottieDrawable() { animator.setRepeatCount(0); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (systemAnimationsAreDisabled) { animator.cancel(); setProgress(1f); } else { setProgress((float) animation.getAnimatedValue()); } } }); } public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { this.progress = progress; if (compositionLayer != null) { compositionLayer.setProgress(progress); } }
可以看到動畫過程中會不斷觸發setProgress,setProgress中呼叫了CompositionLayer的setProgress,然後再通過CompositionLayer把進度傳遞到各個圖層:
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { super.setProgress(progress); if (timeRemapping != null) { long duration = lottieDrawable.getComposition().getDuration(); long remappedTime = (long) (timeRemapping.getValue() * 1000); progress = remappedTime / (float) duration; } if (layerModel.getTimeStretch() != 0) { progress /= layerModel.getTimeStretch(); } progress -= layerModel.getStartProgress(); for (int i = layers.size() - 1; i >= 0; i--) { layers.get(i).setProgress(progress); } }
各個圖層收到通知之後,再處理後回撥給LottieDrawable:
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) { if (progress < getStartDelayProgress()) { progress = 0f; } else if (progress > getEndProgress()) { progress = 1f; } if (progress == this.progress) { return; } this.progress = progress; for (int i = 0; i < listeners.size(); i++) { listeners.get(i).onValueChanged(); } } @Override public void onValueChanged() { invalidateSelf(); } private void invalidateSelf() { lottieDrawable.invalidateSelf(); }
LottieDrawable收到這些圖層的通知就會呼叫invalidateDrawable來不斷重新整理自己:
@Override public void invalidateSelf() { final Callback callback = getCallback(); if (callback != null) { callback.invalidateDrawable(this); } }
以上是Lottie動畫繪製的邏輯,時序圖如下:

Lottie時序圖
那麼Lottie是怎麼將這些Json資料跟動畫關聯起來的呢?After Effect製作動畫的原理是將動畫分為很多個圖層,且每個圖層有各自的細節動畫,這樣疊加合成最終的效果。那麼Lottie也是同理,分為很多個圖層(Layer),我們看下Json的外層結構如下:
{ "v": "4.6.0",//bodymovin的版本 "fr": 24.291553280921,//幀率 "ip": 0,//起始關鍵幀 "op": 84.0029442951,//結束關鍵幀 "w": 100,//動畫寬度 "h": 100,//動畫高度 "ddd": 0, "assets": [...]//資源資訊 "layers": [...]//圖層資訊 }
Lottie會通過Json裡的資料,解析出動畫需要的關鍵幀資訊,並且還有各個圖層的動畫資料,我們依舊是由表及裡,從LottieAnimationView的 setAnimation()
開始看:
public void setAnimation(String animationName) { setAnimation(animationName, defaultCacheStrategy); } public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) { //...省略部分程式碼 compositionLoader = LottieComposition.Factory.fromAssetFileName(getContext(), animationName, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { if (cacheStrategy == CacheStrategy.Strong) { STRONG_REF_CACHE.put(animationName, composition); } else if (cacheStrategy == CacheStrategy.Weak) { WEAK_REF_CACHE.put(animationName, new WeakReference<>(composition)); } setComposition(composition); } }); }
通過工廠模式從LottieComposition的Factory裡讀取我們Asset中的Json檔案,並且將其轉換為LottieComposition物件,那麼看下 LottieComposition
:
static LottieComposition fromJsonSync(Resources res, JSONObject json) { Rect bounds = null; float scale = res.getDisplayMetrics().density; int width = json.optInt("w", -1); int height = json.optInt("h", -1); if (width != -1 && height != -1) { int scaledWidth = (int) (width * scale); int scaledHeight = (int) (height * scale); bounds = new Rect(0, 0, scaledWidth, scaledHeight); } long startFrame = json.optLong("ip", 0); long endFrame = json.optLong("op", 0); float frameRate = (float) json.optDouble("fr", 0); String version = json.optString("v"); String[] versions = version.split("[.]"); int major = Integer.parseInt(versions[0]); int minor = Integer.parseInt(versions[1]); int patch = Integer.parseInt(versions[2]); LottieComposition composition = new LottieComposition( bounds, startFrame, endFrame, frameRate, scale, major, minor, patch); JSONArray assetsJson = json.optJSONArray("assets"); parseImages(assetsJson, composition); parsePrecomps(assetsJson, composition); parseFonts(json.optJSONObject("fonts"), composition); parseChars(json.optJSONArray("chars"), composition); parseLayers(json, composition); return composition; }
LottieComposition
其實就相當於Json和我們動畫屬性的一個轉換器,將我們的Json資料解析成我們需要的記憶體資料,然後根據資料創建出一個或多個圖層,再結合我們剛才分析的屬性動畫,實現最終各種酷炫的效果。
總結
Lottie是一個強大的動畫庫,許多程式碼實現比較複雜或者難以實現的動畫,通過Lottie都能輕易實現,開發效率大大提高,並且後期萬一要更換動畫,同樣十分方便。又或者可以將動畫Json通過網路下發,實現動態更換客戶端的動畫效果。
參考
GitHub: GitHub-ZJYWidget
CSDN部落格: IT_ZJYANG
簡 書: Android小Y
在 GitHub 上建了一個集合炫酷自定義View的專案,裡面有很多實用的自定義View原始碼及demo,會長期維護,歡迎Star~ 如有不足之處或建議還望指正,相互學習,相互進步,如果覺得不錯動動小手給個Star, 謝謝~

關注Android 技術小棧,更多精彩原創