概念
Lottie 是 Airbnb開(kāi)源的一套跨平臺(tái)的完整的動(dòng)畫(huà)效果解決方案,它可以使用Bodymovin解析以json導(dǎo)出的Adobe After Effects動(dòng)畫(huà)襟士,并在移動(dòng)設(shè)備上進(jìn)行本地渲染盗飒。
可以直接運(yùn)用在 iOS
、Android
陋桂、Web
和 React Native
之上逆趣。開(kāi)發(fā)者無(wú)需關(guān)注動(dòng)畫(huà)中的實(shí)現(xiàn)細(xì)節(jié)。
特點(diǎn)
傳統(tǒng)動(dòng)畫(huà)
使用序列幀嗜历,開(kāi)發(fā)者需要設(shè)計(jì)時(shí)序宣渗、位移、透明度梨州、尺寸痕囱、插值器等各類(lèi)參數(shù)信息,再播放出動(dòng)畫(huà)暴匠。lottie的設(shè)計(jì)方案
將設(shè)計(jì)軟件中的時(shí)間軸完整地導(dǎo)出來(lái)鞍恢,包括里面的各種關(guān)鍵幀信息、矢量路徑、層級(jí)有序、樣式等等抹腿。
即動(dòng)畫(huà)的描述文件導(dǎo)出,再將動(dòng)畫(huà)元素導(dǎo)出旭寿,然后在對(duì)應(yīng)的客戶端警绩,解析描述文件,還原出整個(gè)動(dòng)畫(huà)盅称。
- 大小
導(dǎo)出的json文件比gif文件小很多 - 性能
性能也更好(應(yīng)用到矢量圖肩祥,內(nèi)存占用小,縮放效果好) - 使用
API簡(jiǎn)單缩膝,代碼實(shí)現(xiàn)簡(jiǎn)單混狠,開(kāi)發(fā)無(wú)需編寫(xiě)動(dòng)畫(huà),降低動(dòng)畫(huà)的開(kāi)發(fā)成本 - 靈活度
可動(dòng)態(tài)配置下發(fā)疾层,更換替換動(dòng)畫(huà)效果将饺,易于調(diào)試和維護(hù)。 - 適配
不同的手機(jī)分辨率無(wú)需適配 - 通用
跨平臺(tái)痛黎,設(shè)計(jì)稿導(dǎo)出一份動(dòng)畫(huà)描述文件予弧,android,ios湖饱,react native掖蛤,web多端通用 - 效果
幾乎與設(shè)計(jì)出的動(dòng)畫(huà)無(wú)差別
使用流程
使用方法
由于API文檔是1.0.3未及時(shí)更新,建議在源碼里看方法
API文檔
2.7.0版本
動(dòng)畫(huà)來(lái)源可以從本地,網(wǎng)絡(luò)等
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依賴(lài) onDetachedFromWindow停止動(dòng)畫(huà),回收動(dòng)畫(huà)資源
* 但動(dòng)畫(huà)的 play可能是異步的,
* 如果有post出去的異步任務(wù),在detach后動(dòng)畫(huà)仍會(huì)執(zhí)行
*/
cancelAnimation();
}
}
兼容性
版本支持 sdkVersion: >=16
= 2.8以上需AndroidX
依賴(lài)庫(kù)的版本與動(dòng)畫(huà)導(dǎo)出版本有關(guān)
效果支持
引入后的影響
包大小新增: 71K
Json文件結(jié)構(gòu)
- 回憶: 幀動(dòng)畫(huà)的播放.
核心類(lèi)
LottieComposition 將json文件解析成數(shù)據(jù)對(duì)象.
LottieDrawable 承載所有的繪制工作, 將LottieComposition 解析的數(shù)據(jù)對(duì)象, 繪制成 drawable井厌。
LottieAnimationView 提供了異步加載, 反序列化,顯示, 封裝了一些動(dòng)畫(huà)的操作,并處理了圖片的回收onDetachWindow . 控制動(dòng)畫(huà)的實(shí)際操作委托給LottieDrawable
備注:
如直接使用LottieDrawable,需在合適的時(shí)機(jī) invoke recycleBitmaps,否則內(nèi)存泄漏.
動(dòng)畫(huà)播放原理
#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();
}
// 說(shuō)明
BaseLottieAnimator extends ValueAnimator
LottieValueAnimator extends BaseLottieAnimator
BaseLottieAnimator 提供了 notifyStart 蚓庭、notifyEnd、notifyCancel 仅仆、notifyUpdate器赞、 notifyRepeat 等notifyX方法.
通知 所有l(wèi)isteners : ValueAnimator.AnimatorUpdateListener 與ValueAnimator.AnimatorListener
對(duì)應(yīng)者各自的回調(diào)方法 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();
}
動(dòng)畫(huà)適配原理
① Android開(kāi)發(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);
}
小結(jié):
Lottie 適配原理:
解析json文件,獲得取寬高之后, 乘以手機(jī)的相對(duì)密度拳魁。得到初始的Rect邊界.
在使用的時(shí)候判斷適配后的寬高是否超過(guò)屏幕的寬高,如果超過(guò)則再進(jìn)行縮放撮弧。以此保障 Lottie 在 Android 平臺(tái)的顯示效果潘懊。
繪制原理
猜想: 參考動(dòng)畫(huà)播放思路,猜想下繪制流程
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);
}
}
//...
}
遇到的問(wèn)題與解決方案
①依賴(lài)庫(kù)的版本與導(dǎo)出動(dòng)畫(huà)版本
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設(shè)置中啟用“導(dǎo)出為舊格式”
②issues: zip 播放的問(wèn)題
https://github.com/airbnb/lottie-android/issues/1009
③. 內(nèi)存泄漏問(wèn)題
圖片在回收時(shí)機(jī)
@Override protected void onDetachedFromWindow() {
if (isAnimating()) { // 動(dòng)畫(huà)的加載play是異步的
cancelAnimation();
wasAnimatingWhenDetached = true;
}
recycleBitmaps();
super.onDetachedFromWindow();
}
以此可能引發(fā)的內(nèi)存抖動(dòng)的場(chǎng)景
假設(shè)在RecyclerView中使用包涵mattes或者mask的動(dòng)畫(huà)
④ 內(nèi)存抖動(dòng)的風(fēng)險(xiǎn)
bitmap在動(dòng)畫(huà)加載到window時(shí)被創(chuàng)建贸辈,onDetachedFromWindow刪除時(shí)回收释树。所以不宜在RecyclerView中使用包涵mattes或者mask的動(dòng)畫(huà),否則會(huì)引起bitmap抖動(dòng)。
⑤ 版本變更比較多, API變化比較大 解決方案: 封裝,提供統(tǒng)一接口外觀
與SVGA對(duì)比
SVGA里面的每一幀都是關(guān)鍵幀,SVGA已經(jīng)在導(dǎo)出動(dòng)畫(huà)的時(shí)候奢啥,把每一幀的信息都計(jì)算好了秸仙,如此一來(lái),
播放時(shí)無(wú)需關(guān)心插值計(jì)算的過(guò)程桩盲。
通過(guò)幀率去刷每一幀的畫(huà)面寂纪,這個(gè)思路跟gif很像,SVGA可以同時(shí)支持Flash和After Effects的導(dǎo)出.
且通過(guò)配置使得動(dòng)畫(huà)過(guò)程中圖片都可以得到復(fù)用。
- 由于擁有所有幀, 不用解析高階插值(二次線性方程赌结,貝塞爾曲線方程),節(jié)省了CPU
- 2.x之后的svga捞蛋,使用Protocol Buffers 來(lái)做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高柬姚。
Lottie關(guān)鍵幀Keyframes, 是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu
與SVGA對(duì)比
SVGA動(dòng)畫(huà)原理
逐幀渲染拟杉,每一幀均為關(guān)鍵幀,只需渲染每個(gè)元素?zé)o需插值計(jì)算
播放前一次性上傳紋理到 GPU量承,并在動(dòng)畫(huà)過(guò)程中復(fù)用紋理
2.x之后的svga搬设,使用Protocol Buffers 來(lái)做序列化,序列化的數(shù)據(jù)體更小,傳遞效率比xml,json 更高宴合。
Lottie動(dòng)畫(huà)原理
逐層渲染焕梅,完全按照設(shè)計(jì)工具的設(shè)計(jì)思路還原
播放解析多個(gè)圖層配置并添加相應(yīng)動(dòng)畫(huà),并在動(dòng)畫(huà)過(guò)程中復(fù)用圖層
當(dāng)需要解析高階插值卦洽,性能相對(duì)差一些 (關(guān)鍵幀Keyframes,是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu)
通過(guò)幀率去刷每一幀的畫(huà)面,與gif很像,所以SVGA可以同時(shí)支持Flash和After Effects的導(dǎo)出.
- 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(jié)省了CPU
Lottie關(guān)鍵幀Keyframes, 是通過(guò)傳參的方式,交由cpu去運(yùn)算. 所以復(fù)雜動(dòng)畫(huà)實(shí)現(xiàn)耗費(fèi)cpu
位圖與json
SVGA是將圖片與描述文件集成在.svga文件當(dāng)中的斜棚,而Lottie則是把二者分離開(kāi)阀蒂。
Lottie可以在導(dǎo)出后,再對(duì)圖片進(jìn)行文件大小優(yōu)化弟蚀;而SVGA最好是在事先就對(duì)圖片進(jìn)行大小優(yōu)化蚤霞。
SVGA應(yīng)用場(chǎng)景:
在直播應(yīng)用場(chǎng)景,禮物播放,游戲炫酷動(dòng)畫(huà).
Lottie應(yīng)用場(chǎng)景:
高德地圖,支付寶,全民K歌
阿里提供的犸良動(dòng)畫(huà)
它最底層采用的技術(shù)就是Lottie,阿里對(duì)其二次封裝了許多預(yù)設(shè)的動(dòng)畫(huà)效果义钉,
可以自定義其中的元素與參數(shù)昧绣,然后試著導(dǎo)出你的第一個(gè)json文件~
文獻(xiàn)參考
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
導(dǎo)出工具
https://github.com/bodymovin/bodymovin
動(dòng)畫(huà)預(yù)覽器
https://lottiefiles.com/preview
https://airbnb.design/lottie/
SVGA生態(tài)
開(kāi)源iOS/Android/Web三個(gè)平臺(tái)的源碼。
https://github.com/yyued/SVGAPlayer-Android 2.5K star
https://github.com/yyued/SVGAPlayer-iOS
https://github.com/yyued/SVGAPlayer-Web
設(shè)計(jì)師工具
http://svga.io/designer.html
動(dòng)畫(huà)預(yù)覽器
http://svga.io/svga-preview.html