隨著移動網際網路的發展,越來越多的移動 APP 都會從互動視覺方面來提升使用者體驗,其中提供炫酷的動畫效果是一個經常使用的方法。然而,眾所周知,Android平臺中的動畫效果的實現一直以來都存在一些痛點,而這些痛點也給 Android平臺應用上實現豐富動畫效果帶來了非常大的困難。本文將會提及一種更先進的動畫框架,通過這個框架可以方便實現各種炫酷的動畫效果,而且相比傳統動畫效果實現方法,有非常大的優勢,各位看官,不要著急,請接著慢慢往下看!
一. 傳統動效實現
傳統動畫效果的實現方法,歸納總結起來,大概有這麼幾種方案:
1. 程式碼實現
通過 Android 系統提供的動畫介面,通過程式碼來實現動畫效果,一般情況下,這種動畫效果都是比較簡單的,例如實現一個簡單的平移、綻放,旋轉等,如果這三種效果組合起來,將會變得非常複雜,因此,程式碼實現一般也僅適用於簡單動效的開發,如果太複雜的動效的話,那麼投入產出比將會嚴重不平衡。
通過程式碼實現,也又分兩種:
- 純程式碼實現:使用 Animation 類或 Property Animation 來實現。
- 序列幀實現:設計師提供一系列的序列幀圖片,開發者將這些圖片封裝成 xml animation,然後在程式碼中載入該 xml,從而實現動畫效果。
這種方式的實現的缺點:
- 不靈活:程式碼實現複雜,動畫效果的更改,將更改程式碼。
- 開發效率低下:需要多次除錯和修改,才能達到最終的實現效果。
- 記憶體開銷大:序列幀是多張圖片實現,記憶體開銷大。
2. Gif 動態圖
通過 Gif 圖片可以實現動效,但是最大的問題是:
- 記憶體大
- 不能控制動畫,例如暫停,開始
3. 小視訊
播放一個輕量的小視訊,這種也能達到效果,但最大的問題是:
- 相容性,視訊播放特殊性可能引發相容的問題
- View 不好控制,可能會影響到音訊等
總結起來,傳統動效實現的流程如下:
視覺出設計稿 ——> 輸出 n 張圖 ——> 研發實現序列幀 ——> 執行看效果/適配 ——> 調整程式碼再看效果 ——> ...
所下圖所示:
由此可以看出,開發者實現一個動畫非常繁瑣複雜,而且每增加一種動畫,需要重新開發。
二. 傳統動效實現的缺點
綜合看來,以上幾種方案都有非常明顯的缺點,再次概括一下,傳統動畫效果的缺點有
1. 效率低
設計師要出多終序列幀圖片,工作量大;研發需要針對這些序列幀來實現,反覆除錯,開發工作量大,從而導致一個動畫效果的實現的整體效率非常低。
2. 不靈活
要更新一個動畫,需要研發再次修改程式碼,再反覆除錯,非常不靈活。
那針對這些問題,有沒有一種能全新的解決方案來解決這些問題,使動畫效果的實現更加優雅更高效呢?接下來就是本文的重點 —— Airbnb Lottie
動畫庫登場!
三. Airbnb Lottie動畫框架的實
關於 lottie
(github.com/airbnb/lott… ), 它提供了優雅的解決方案,通過這個框架,可以做出非常豐富的炫酷的動畫效果。先來看一下官方的效果圖:
1. 基於 lottie 動效實現流程
- 第一步: 設計師設計動畫效果,再通過外掛(Adobe After Effects)將動畫效果匯出成一個動畫描述檔案
- 第二步: 將這個動畫描述檔案,預置在應用的
assets
目錄下,通過lottie
框架載入這些動畫檔案。 - 第三步: 執行,檢視效果
概括起來,如下圖所示:
2. Lottie的優勢
相比傳統的實現方案,具備非常大的優勢。
a) 高效
開發者一次開發,可以多次複用,不需要再去寫各種具體的動畫相關的程式碼。設計師設計好動畫效果之後,匯出檔案即可。
b) 靈活
由於動畫通過檔案來描述,替換不同的檔案,將會得到不同的動畫效果,動效的更新或升級,將非常靈活。
c) 資料來源多樣性
既然是載入動畫描述檔案,那麼這個檔案就可以從任意地方來,assets、sdcard、network都是可以的。從網路載入動畫描述檔案,將能做到不發版的情況下,動態更新動效。可以從網路下載動畫檔案,從而可以快速做A/B test。
d) 跨平臺
動畫檔案可以應用於 Android、iOS、React Native,這樣設計師只需出一份動效設計稿就行,不用區分平臺。
不同的動畫效果,只需要做一次開發即可
四. Lottie實現原理
1. 開發流程圖
2. 實現原理
Lottie使用json檔案來作為動畫資料來源,json檔案是通過Bodymovin外掛匯出的,檢視sample中給出的json檔案,其實就是把圖片中的元素進行來拆分,並且描述每個元素的動畫執行路徑和執行時間。Lottie的功能就是讀取這些資料,然後繪製到螢幕上。
Lottie 提供了一個 LottieAnimationView 給使用者使用,而實際 Lottie 的核心是 LottieDrawable,它承載了所有的繪製工作,LottieAnimationView則是對LottieDrawable 的封裝,再附加了一些例如 解析 的功能。
LottieComposition 是 對應的 Model,承載 的所有資訊。
CompositionLayer 是 layer 的集合。
ImageAssetBitmapManager 負責管理動畫所需的圖片資源。
它們的關係:
3. 資料解析
首先要解析json,建立資料到物件的對映,然後根據資料物件建立合適的Drawable繪製到view上,動畫的實現可以通過操作讀取到的元素完成。
具體過程如下所示:
json檔案 ——> Componsition ——> Drawable ——> View
通過如下3個核心類來來完成整個工作流程,因而使用起來比較簡單。以下是三個比較核心的類的說明:
- LottieComposition (json->資料物件)
Lottie使用LottieComposition來作為After Effects的資料物件,即把Json檔案對映為到LottieComposition,該類中提供瞭解析json的靜態方法。
- LottieDrawable (資料物件->Drawable)
這個類是最上層使用的重要的一個類,動畫的繪製就是由這個類來實現。
- LottieAnimationView(繪製)
操作集合,LottieAnimationView 繼承自 AppCompatImageView,封裝了一些動畫的操作,具體的繪製時委託為 LottieDrawable 完成的。
資料的解析,主要參考 LottieComposition.fromJsonSync
方法:
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);
int frameRate = json.optInt("fr", 0);
LottieComposition composition =
new LottieComposition(bounds, startFrame, endFrame, frameRate, scale);
JSONArray assetsJson = json.optJSONArray("assets");
parseImages(assetsJson, composition);
parsePrecomps(assetsJson, composition);
parseLayers(json, composition);
return composition;
}
複製程式碼
還有 parseImages
、parsePrecomps
、 parseLayers
這幾個方法。
4. JSON檔案的屬性含義
動畫描述檔案是通過 bodymovin
外掛匯出來的,裡面包含了動畫的一切資訊,包括了幀率,動畫形態,如何做動畫等。接下來將簡單說明一下動畫描述檔案中的主要屬性。
最外部結構:
{
"v": "4.6.2",
"fr": 25,
"ip": 0,
"op": 1000,
"w": 720,
"h": 800,
"nm": "合成 1",
"ddd": 0,
"assets":[],
"layers":[]
}
複製程式碼
屬性的含義:
屬性 | 含義 |
---|---|
v | bodymovin的版本 |
fr | 幀率 |
ip | 起始關鍵幀 |
op | 結束關鍵幀 |
w | 動畫寬度 |
h | 動畫高度 |
assets | 動畫圖片資源資訊 |
layers | 動畫圖層資訊 |
從這裡可以獲取 設計的動畫的寬高,幀相關的資訊,動畫所需要的圖片資源的資訊以及圖層資訊。
assets
圖片資源資訊, 相關類 LottieImageAsset、 ImageAssetBitmapManager。
"assets": [
{
"id": "image_0",
"w": 58,
"h": 31,
"u": "images/",
"p": "img_0.png"
}
}
複製程式碼
屬性的含義:
屬性 | 含義 |
---|---|
id | 圖片id |
w | 圖片寬度 |
h | 圖片高度 |
p | 圖片名稱 |
layers
圖層資訊,相關類:Layer、BaseLayer以及 BaseLayer 的實現類。
{
"ddd": 0,
"ind": 0,
"ty": 2,
"nm": "btnSlice.png",
"cl": "png",
"refId": "image_0",
"ks": {....},
"ao": 0,
"ip": 0,
"op": 90.0000036657751,
"st": 0,
"bm": 0,
"sr": 1
}
複製程式碼
屬性 | 含義 |
---|---|
nm | layerName 圖層資訊 |
refId | 引用的資源 id,如果是 ImageLayer 那麼就是圖片的id |
ty | layertype 圖層型別 |
ip | inFrame 該圖層起始關鍵幀 |
op | outFrame 該圖層結束關鍵幀 |
st | startFrame 開始 |
ind | layer id 圖層 id |
Layer 可以理解為圖層,跟 PS 等工具的概念相同,每個 Layer 負責繪製自己的內容。
在 Lottie 裡擁有不同的 Layer,目前有 PreComp,Solid,Image,Null,Shape,Text ,各個 Layer 擁有的屬性各不相同,這裡只指出共有的屬性。
下圖為 Layer 相關類圖:
5. 動畫如何動起來的?
還有一個問題,動畫是如何動起來的呢?這裡用到了屬性動畫來產生一個0~1的插值,根據不同的插值來播放不同幀的動畫,說白了,就是呼叫 setProgress(float)
來做動畫。程式碼如下:
private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
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());
}
}
});
}
複製程式碼
6. lottie適配原理
內部適配邏輯如下:
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);
}
...
}
複製程式碼
LottieAnimationView#setComposition:
public void setComposition(@NonNull LottieComposition composition) {
if (L.DBG) {
Log.v(TAG, "Set Composition \n" + composition);
}
lottieDrawable.setCallback(this);
boolean isNewComposition = lottieDrawable.setComposition(composition);
if (!isNewComposition) {
// We can avoid re-setting the drawable, and invalidating the view, since the composition
// hasn't changed.
return;
}
int screenWidth = Utils.getScreenWidth(getContext());
int screenHeight = Utils.getScreenHeight(getContext());
int compWidth = composition.getBounds().width();
int compHeight = composition.getBounds().height();
if (compWidth > screenWidth ||
compHeight > screenHeight) {
float xScale = screenWidth / (float) compWidth;
float yScale = screenHeight / (float) compHeight;
float maxScaleForScreen = Math.min(xScale, yScale);
setScale(Math.min(maxScaleForScreen, lottieDrawable.getScale()));
Log.w(L.TAG, String.format(
"Composition larger than the screen %dx%d vs %dx%d. Scaling down.",
compWidth, compHeight, screenWidth, screenHeight));
}
...
}
複製程式碼
7. 播放動畫時序圖
播放動畫的時序圖如下所示:
其中 BaseKeyframeAnimation
類是比較核心的,它根據當前的 progress,來計算所需要的值。 關於 BaseKeyframeAnimation
的繼承關係圖如下:
五. lottie vs keyframes
lottie
由 Airbnb 出品,而keyframes
由 facebook 出品,這兩個庫實現效果都差不多。據 lottie 官網說功能比 keyframes 強大一些。感興趣的看官可以去深入研究一下。
關於 keyframes 的介紹,請參考:
facebookincubator.github.io/Keyframes/
六. 後續思考
1. 更酷的使用者引導
App的使用者引導完成可以用 lottie 來實現,介面的圖形可以跟隨手勢滑動而變化,這樣的體驗會更好,更新穎。
2. 動效工具
完全可以開發一個預覽動效的工具,提供給視覺設計師,比如,帶一個二維碼掃描功能,設計師設計好動效後,用這個app掃描二維碼,把動畫描述檔案下載到本地,就可以立即看到動畫在App中的效果是什麼樣。
3. 更多動效成為可能
基於這樣的動畫框架,App中可以實現更多更豐富的動畫效果,這將會大大提升使用者體驗。
七. 參考資料
- lottie-android
- introducing-lottie
- design lottie
- bodymovin
- Animation: Jump-through
- keyframes overview
- Lottie實現原理要點分析