概念
Lottie 是 Airbnb開源的一套跨平臺的完整的動畫效果解決方案,它可以使用Bodymovin解析以json導出的Adobe After Effects動畫,并在移動設備上進行本地渲染。
可以直接運用在 iOS、Android、Web 和 React Native之上。開發(fā)者無需關注動畫中的實現(xiàn)細節(jié)。
特點
傳統(tǒng)動畫
使用序列幀,開發(fā)者需要設計時序、位移、透明度、尺寸、插值器等各類參數(shù)信息,再播放出動畫。lottie的設計方案
將設計軟件中的時間軸完整地導出來,包括里面的各種關鍵幀信息、矢量路徑、層級、樣式等等。
即動畫的描述文件導出,再將動畫元素導出,然后在對應的客戶端,解析描述文件,還原出整個動畫。
- 大小
導出的json文件比gif文件小很多 - 性能
性能也更好(應用到矢量圖,內存占用小,縮放效果好) - 使用
API簡單,代碼實現(xiàn)簡單,開發(fā)無需編寫動畫,降低動畫的開發(fā)成本 - 靈活度
可動態(tài)配置下發(fā),更換替換動畫效果,易于調試和維護。 - 適配
不同的手機分辨率無需適配 - 通用
跨平臺,設計稿導出一份動畫描述文件,android,ios,react native,web多端通用 - 效果
幾乎與設計出的動畫無差別
使用流程

使用方法
由于API文檔是1.0.3未及時更新,建議在源碼里看方法
API文檔
2.7.0版本
動畫來源可以從本地,網(wǎng)絡等
public void setAnimation(@RawRes final int rawRes) //src/main/res/raw
public void setAnimation(final String assetName) //src/main/assets
public void setAnimationFromJson(String jsonString)
public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
public void setAnimation(JsonReader reader, @Nullable String cacheKey) //JSON文件或zip文件的InputStream
public void setAnimationFromUrl(String url) //json或zip文件的網(wǎng)址
public boolean addLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
public boolean removeLottieOnCompositionLoadedListener(lottieOnCompositionLoadedListener)
public void playAnimation()
public void pauseAnimation()
public void setProgress(float progress)
...
public void setImageBitmap(Bitmap bm)
public void setImageResource(int resId)
public void setImageDrawable(Drawable drawable)
...
AnimatorListener 、AnimatorUpdateListener 接口的支持
使用示例
<....ui.lottie.RecyclableLottieAnimationView
android:id="@+id/title_bar_toolbox_ani"
android:layout_width="@dimen/titlebar_web_action_width"
android:layout_height="@dimen/titlebar_web_action_width"
android:layout_centerVertical="true"
android:layout_alignParentRight="true">
mToolBoxAniView = mTopView.findViewById(R.id.title_bar_toolbox_ani);
mToolBoxAniView.setAnimation("sniffer_animation.json");
mToolBoxAniView.playAnimation();
public class RecyclableLottieAnimationView extends LottieAnimationView {
public RecyclableLottieAnimationView(Context context) {
super(context);
}
public RecyclableLottieAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RecyclableLottieAnimationView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
/**
* lottie依賴 onDetachedFromWindow停止動畫,回收動畫資源
* 但動畫的 play可能是異步的,
* 如果有post出去的異步任務,在detach后動畫仍會執(zhí)行
*/
cancelAnimation();
}
}
兼容性
版本支持 sdkVersion: >=16
= 2.8以上需AndroidX
依賴庫的版本與動畫導出版本有關
效果支持
引入后的影響
包大小新增: 71K
Json文件結構

- 回憶: 幀動畫的播放.
核心類
LottieComposition 將json文件解析成數(shù)據(jù)對象.
LottieDrawable 承載所有的繪制工作, 將LottieComposition 解析的數(shù)據(jù)對象, 繪制成 drawable。
LottieAnimationView 提供了異步加載, 反序列化,顯示, 封裝了一些動畫的操作,并處理了圖片的回收onDetachWindow . 控制動畫的實際操作委托給LottieDrawable
備注:
如直接使用LottieDrawable,需在合適的時機 invoke recycleBitmaps,否則內存泄漏.


動畫播放原理
#LottieAnimationView.java
@MainThread
public void playAnimation() {
lottieDrawable.playAnimation();
enableOrDisableHardwareLayer();
}
#LottieDrawable.java
@MainThread
public void playAnimation() {
if (compositionLayer == null) {
lazyCompositionTasks.add(new LazyCompositionTask() {
@Override public void run(LottieComposition composition) {
playAnimation();
}
});
return;
}
animator.playAnimation(); // LottieValueAnimator
}
#LottieValueAnimator.java
@MainThread
public void playAnimation() {
running = true;
notifyStart(isReversed());
// update frame and notifyUpdate
setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
lastFrameTimeNs = System.nanoTime();
repeatCount = 0;
postFrameCallback();
}
// 說明
BaseLottieAnimator extends ValueAnimator
LottieValueAnimator extends BaseLottieAnimator
BaseLottieAnimator 提供了 notifyStart 、notifyEnd、notifyCancel 、notifyUpdate、 notifyRepeat 等notifyX方法.
通知 所有l(wèi)isteners : ValueAnimator.AnimatorUpdateListener 與ValueAnimator.AnimatorListener
對應者各自的回調方法 eg: onAnimationStart 、onAnimationEnd、onAnimationX方法
#LottieDrawable.java
public LottieDrawable() {
this.animator.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
if (LottieDrawable.this.compositionLayer != null) {
LottieDrawable.this.compositionLayer.setProgress(LottieDrawable.this.animator.getAnimatedValueAbsolute());
}
}
});
}
#CompositionLayer.java
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
//... update the progress
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
#BaseLayer.java
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
//... layout.setProgress
for (int i = 0; i < animations.size(); i++) {
// List<BaseKeyframeAnimation<?, ?>> animations
animations.get(i).setProgress(progress);
}
}
#BaseKeyframeAnimation.java
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
//... update the progress
notifyListeners();
}
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
#BaseLayer
@Override public void onValueChanged() {
invalidateSelf();
}
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}

動畫適配原理
① Android開發(fā)中, 不同屏幕分辨率適配.
② 為自定義View為其添加縮放屬性
LottieAnimationView.java
private void init(@Nullable AttributeSet attrs) {
...
if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) {
lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f));
}
...
}
LottieAnimationView$setScale 委托給LottieDrawable
#LottieDrawable.java
public void setScale(float scale) {
this.scale = scale;
updateBounds();
}
#LottieDrawable.java
private void updateBounds() {
if (composition == null) {
return;
}
float scale = getScale();
// Drawable#setBounds
setBounds(0, 0, (int) (composition.getBounds().width() * scale),
(int) (composition.getBounds().height() * scale));
}
#LottieDrawable.java
private float getMaxScale(@NonNull Canvas canvas) {
float maxScaleX = canvas.getWidth() / (float) composition.getBounds().width();
float maxScaleY = canvas.getHeight() / (float) composition.getBounds().height();
return Math.min(maxScaleX, maxScaleY);
}
@Override public void draw(@NonNull Canvas canvas) {
float scale = this.scale;
float extraScale = 1f;
float maxScale = getMaxScale(canvas);
if (scale > maxScale) {
scale = maxScale;
extraScale = this.scale / scale;
}
if (extraScale > 1) {
// ... translate and scale
}
matrix.reset();
matrix.preScale(scale, scale);
compositionLayer.draw(canvas, matrix, alpha);
}
#LottieCompositionParser.java
public static LottieComposition parse(JsonReader reader) throws IOException {
float scale = Utils.dpScale();
// ...parse
int scaledWidth = (int) (width * scale);
int scaledHeight = (int) (height * scale);
Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
images, characters, fonts);
}
小結:
Lottie 適配原理:
解析json文件,獲得取寬高之后, 乘以手機的相對密度。得到初始的Rect邊界.
在使用的時候判斷適配后的寬高是否超過屏幕的寬高,如果超過則再進行縮放。以此保障 Lottie 在 Android 平臺的顯示效果。
繪制原理
猜想: 參考動畫播放思路,猜想下繪制流程
BaseLayer.java
@Override
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
if (!visible) {
return;
}
buildParentLayerListIfNeeded();
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
matrix.preConcat(transform.getMatrix());
drawLayer(canvas, matrix, alpha);
recordRenderTime(L.endSection(drawTraceName));
return;
}
if (hasMasksOnThisLayer()) {
//...draw maskLayer
applyMasks(canvas, matrix);
}
if (hasMatteOnThisLayer()) {
//...draw matteLayer
matteLayer.draw(canvas, parentMatrix, alpha);
}
}
CompositionLayer.java
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
//...
for (int i = layers.size() - 1; i >= 0 ; i--) {
boolean nonEmptyClip = true;
if (!newClipRect.isEmpty()) {
nonEmptyClip = canvas.clipRect(newClipRect);
}
if (nonEmptyClip) {
BaseLayer layer = layers.get(i);
layer.draw(canvas, parentMatrix, parentAlpha);
}
}
//...
}
遇到的問題與解決方案
①依賴庫的版本與導出動畫版本
java.lang.IllegalStateException: Missing values for keyframe.
at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:16)
at com.airbnb.lottie.animation.keyframe.FloatKeyframeAnimation.getValue(FloatKeyframeAnimation.java:8)
at com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation.getValue(BaseKeyframeAnimation.java:125)
at com.airbnb.lottie.animation.keyframe.TransformKeyframeAnimation.getMatrix(TransformKeyframeAnimation.java:113)
原因:
json格式與解析規(guī)則不匹配
Lottie 3.0和Bodymovin 5.5有一些重要的json優(yōu)化,可以節(jié)省json大小和解析速度的1/3。 但是,必須在3.0以上生效,否則就在bodymovin設置中啟用“導出為舊格式”
②issues: zip 播放的問題
https://github.com/airbnb/lottie-android/issues/1009
③. 內存泄漏問題
圖片在回收時機
@Override protected void onDetachedFromWindow() {
if (isAnimating()) { // 動畫的加載play是異步的
cancelAnimation();
wasAnimatingWhenDetached = true;
}
recycleBitmaps();
super.onDetachedFromWindow();
}
以此可能引發(fā)的內存抖動的場景
假設在RecyclerView中使用包涵mattes或者mask的動畫
④ 內存抖動的風險
bitmap在動畫加載到window時被創(chuàng)建,onDetachedFromWindow刪除時回收。所以不宜在RecyclerView中使用包涵mattes或者mask的動畫,否則會引起bitmap抖動。
⑤ 版本變更比較多, API變化比較大 解決方案: 封裝,提供統(tǒng)一接口外觀
與SVGA對比
SVGA里面的每一幀都是關鍵幀,SVGA已經(jīng)在導出動畫的時候,把每一幀的信息都計算好了,如此一來,
播放時無需關心插值計算的過程。
通過幀率去刷每一幀的畫面,這個思路跟gif很像,SVGA可以同時支持Flash和After Effects的導出.
且通過配置使得動畫過程中圖片都可以得到復用。
- 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(jié)省了CPU
- 2.x之后的svga,使用Protocol Buffers 來做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高。
Lottie關鍵幀Keyframes, 是通過傳參的方式,交由cpu去運算. 所以復雜動畫實現(xiàn)耗費cpu
與SVGA對比
SVGA動畫原理
逐幀渲染,每一幀均為關鍵幀,只需渲染每個元素無需插值計算
播放前一次性上傳紋理到 GPU,并在動畫過程中復用紋理
2.x之后的svga,使用Protocol Buffers 來做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高。
Lottie動畫原理
逐層渲染,完全按照設計工具的設計思路還原
播放解析多個圖層配置并添加相應動畫,并在動畫過程中復用圖層
當需要解析高階插值,性能相對差一些 (關鍵幀Keyframes,是通過傳參的方式,交由cpu去運算. 所以復雜動畫實現(xiàn)耗費cpu)
通過幀率去刷每一幀的畫面,與gif很像,所以SVGA可以同時支持Flash和After Effects的導出.
- 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(jié)省了CPU
Lottie關鍵幀Keyframes, 是通過傳參的方式,交由cpu去運算. 所以復雜動畫實現(xiàn)耗費cpu
位圖與json
SVGA是將圖片與描述文件集成在.svga文件當中的,而Lottie則是把二者分離開。
Lottie可以在導出后,再對圖片進行文件大小優(yōu)化;而SVGA最好是在事先就對圖片進行大小優(yōu)化。
SVGA應用場景:
在直播應用場景,禮物播放,游戲炫酷動畫.
Lottie應用場景:
高德地圖,支付寶,全民K歌
阿里提供的犸良動畫
它最底層采用的技術就是Lottie,阿里對其二次封裝了許多預設的動畫效果,
可以自定義其中的元素與參數(shù),然后試著導出你的第一個json文件~
文獻參考
Lottie生態(tài)
https://github.com/airbnb/lottie-android 31.1K star
https://github.com/airbnb/lottie-ios
https://github.com/airbnb/lottie-web
Lottie doc
導出工具
https://github.com/bodymovin/bodymovin
動畫預覽器
https://lottiefiles.com/preview
https://airbnb.design/lottie/
SVGA生態(tài)
開源iOS/Android/Web三個平臺的源碼。
https://github.com/yyued/SVGAPlayer-Android 2.5K star
https://github.com/yyued/SVGAPlayer-iOS
https://github.com/yyued/SVGAPlayer-Web