從json文件到炫酷動(dòng)畫-Lottie實(shí)現(xiàn)思路和源碼分析

Lottie是最近Airbnb開源的動(dòng)畫項(xiàng)目,支持Android、iOS、ReactNaitve三個(gè)平臺(tái),相關(guān)背景介紹可以參考之前的文章Airbnb開源炫酷動(dòng)畫庫Lottie(譯)-看看Airbnb的工程師怎么說。本文分析主要Lottie把json文件轉(zhuǎn)為動(dòng)畫的思路和源碼實(shí)現(xiàn)。

文章首先介紹Lottie的基本使用,然后分析把json文件映射到動(dòng)畫的實(shí)現(xiàn)思路,最后分析Lottie的源碼實(shí)現(xiàn),這里分析的是Lottie-Android。

基本用法

與使用相關(guān)的只有三個(gè)類文件:LottieAnimationView、LottieComposition、LottieDrawable,所以Lottie使用起來特別簡單(需要注意Lottie支持API16及以上)。
最簡單的使用方式是在xml中增加LottieAnimationView:

"Logo/LogoSmall.json"是需要加載的動(dòng)畫數(shù)據(jù)路徑,根目錄是assets目錄。

也可以通過代碼設(shè)置動(dòng)畫數(shù)據(jù)json路徑:

然后在代碼中控制動(dòng)畫播放或者添加監(jiān)聽事件:

Lottie提供了LottieDrawable可以使用:


可以看到Lottie使用起來非常簡單,我們之后就從以上用到的LottieAnimationView、LottieComposition、LottieDrawable入手來分析下Lottie動(dòng)畫的實(shí)現(xiàn)原理。

思路分析

我們先從底層思考下如何在屏幕上繪制動(dòng)畫,最簡單的方式是把動(dòng)畫分為多張圖片,然后通過周期替換屏幕上繪制的圖片來形成動(dòng)畫,這種暴力的方式非常簡單,但缺點(diǎn)明顯,很耗內(nèi)存,動(dòng)畫播放中前后兩張?zhí)鎿Q的圖片在很多元素并沒有變化,重復(fù)的內(nèi)容浪費(fèi)了空間。

為了提高空間利用率,可以把圖片中的元素進(jìn)行拆分,使用過photoshop的同學(xué)知道,其實(shí)在處理一張圖片時(shí),可以把一張復(fù)雜的圖片使用多個(gè)圖層來表示,每個(gè)圖層上展示一部分內(nèi)容,圖層中的內(nèi)容也可以拆分為多個(gè)元素。拆分元素之后,根據(jù)動(dòng)畫需求,可以單獨(dú)對(duì)圖層,甚至圖層中的元素設(shè)置平移、旋轉(zhuǎn)、收縮等動(dòng)畫。

Lottie使用json文件來作為動(dòng)畫數(shù)據(jù)源,json文件是通過Bodymovin插件導(dǎo)出的,查看sample中給出的json文件,其實(shí)就是把圖片中的元素進(jìn)行來拆分,并且描述每個(gè)元素的動(dòng)畫執(zhí)行路徑和執(zhí)行時(shí)間。Lottie的功能就是讀取這些數(shù)據(jù),然后繪制到屏幕上。

現(xiàn)在思考如果我們拿到一份json格式動(dòng)畫如何展示到屏幕上。首先要解析json,建立數(shù)據(jù)到對(duì)象的映射,然后根據(jù)數(shù)據(jù)對(duì)象創(chuàng)建合適的Drawable繪制到View上,動(dòng)畫的實(shí)現(xiàn)可以通過操作讀取到的元素完成。

源碼分析

1. json文件到對(duì)象的映射

Lottie使用LottieComposition來作為After Effects的數(shù)據(jù)對(duì)象,即把json文件映射到LottieCompositionLottieComposition中提供了解析json的靜態(tài)方法:

我們看下LottieComposition都有哪些成員變量,這些成員變量描述了After Effects中的動(dòng)畫。

可以看到startFrame、endFrame、duration、scale等都是動(dòng)畫中常見的。我們看下List<Layer>,看名字就是映射拆分后的圖層數(shù)據(jù):

Layer 中完成layer的json數(shù)據(jù)解析:

2. 數(shù)據(jù)對(duì)象到Drawable的映射

AnimatableLayer 繼承自 Drawable,我們看下它的子類:

其中LayerView對(duì)應(yīng)著Layer數(shù)據(jù),Layer中有


對(duì)應(yīng)的LayerView中有

可以簡單地理解為ViewGroup中可以包含ViewGroup或者View,但其實(shí)整個(gè)Lottie實(shí)現(xiàn)的動(dòng)畫都是繪制在一個(gè)View LottieAnimationView上。

AnimatableLayer 的其它子類如 ShapeLayer,RectLayouer等作為 LayerViewList<AnimatableLayer>的元素。

3. 繪制

LottieAnimationView 繼承自 AppCompatImageView,封裝了一些動(dòng)畫的操作,如:

具體的繪制時(shí)委托為 LottieDrawable 完成的,我們看下 LottieDrawable 中的 draw() 方法:

LottieDrawable 繼承自AnimatableLayer,其draw()方法如下:

可以看到先繪制了本層的內(nèi)容,然后開始繪制包含的layers的內(nèi)容:

這個(gè)過程于界面中ViewGroup嵌套繪制類似。

實(shí)現(xiàn)分析

上面我們根據(jù)動(dòng)畫繪制的思路分析了下Lottie實(shí)現(xiàn)機(jī)制,下面從正面來捋一下程序的執(zhí)行過程:

  1. 創(chuàng)建LottieAnimationView lottieAnimationView
  2. 創(chuàng)建LottieDrawable lottieDrawable
  3. 使用LottieComposition中的靜態(tài)方法解析json文件創(chuàng)建LottieComposition lottieComposition,這個(gè)過程中已經(jīng)創(chuàng)建來多個(gè)Layer對(duì)象。
  4. lottieDrawable.setComposition(lottieComposition)

先清理之前的數(shù)據(jù),然后開始buildLayersForComposition,即根據(jù)lottieComposition建立多個(gè)layerView,此時(shí)已經(jīng)創(chuàng)建好了多個(gè)Drawable,并通過List建立的為以lottieDrawable為根的一個(gè)drawable樹。

  1. lottieAnimationView.setImageDrawable(lottieDrawable)

  2. lottieAnimationView.playAnimation()

直接委托給了lottieDrawablelottieDrawable中有private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);

重點(diǎn)看下setProgress方法

調(diào)用了private final List<KeyframeAnimation<?>> animations = new ArrayList<>()setProgress

onValueChanged時(shí),各個(gè)創(chuàng)建好的Drawable會(huì)根據(jù)需求進(jìn)行重繪,達(dá)到動(dòng)畫的效果。

Lottie把動(dòng)畫從View的動(dòng)效轉(zhuǎn)移到了Drawable上。

Lottie的性能

可以看到Lottie把json描述的動(dòng)畫數(shù)據(jù)映射到Drawable之后,實(shí)現(xiàn)動(dòng)畫時(shí)用到了ValueAnimator,在動(dòng)畫更新時(shí)使用Drawable而非View,個(gè)人感覺在不需要交互時(shí)Drawable顯然比View更加輕量。以下是Lottie性能的官方的說明:

  1. 如果沒有mask和mattes,那么性能和內(nèi)存非常好,沒有bitmap創(chuàng)建,大部分操作都是簡單的cavas繪制。
  2. 如果存在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ì)于簡單的動(dòng)畫,在實(shí)際使用時(shí)性能不太明顯。
  3. 如果在列表中使用動(dòng)畫,推薦使用緩存LottieAnimationView.setAnimation(String, CacheStrategy) 。

歡迎關(guān)注公眾號(hào)wutongke,每天推送移動(dòng)開發(fā)前沿技術(shù)文章:

wutongke

推薦閱讀:

Airbnb開源炫酷動(dòng)畫庫Lottie(譯)-看看Airbnb的工程師怎么說

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容