回答這個問題前,咱們先想一想下面這些動畫須要怎麼實現:java
是否是一臉懵逼,若是不懵逼是否是感受壓力山大?傳統方式實現動畫,無非如下幾種方式:android
那麼有什麼方法既能夠高效的實現動畫,又不須要佔用過多空間,還能同時支持多個系統環境呢?Lottie應運而生。git
Lottie 是Airbnb開源的動畫實現項目,支持Android、iOS、ReactNaitve三大平臺,Github原文內容請點擊這裏。Lottie 的使用前提是須要先經過插件 bodymovin 將 Adobe After Effects (AE)生成的 aep 動畫工程文件轉換爲通用的 json 格式描述文件( bodymovin 插件自己是用於網頁上呈現各類AE效果的一個開源庫)。Lottie 所作的事情就是實如今不一樣移動端平臺上呈現AE動畫的方式,從而達到動畫文件的一次繪製、一次轉換,隨處可用的效果。github
本文主要側重於講解 Lottie 在Android 中的使用方式及源碼實現過程,關於 AE 的安裝和使用過程請點擊這裏。json
現有版本已升級到2.0.0-rc1canvas
dependencies { compile 'com.airbnb.android:lottie:2.0.0-rc1' }
Lottie支持ICS (API 14)及以上的系統版本, 最簡單的使用方式是直接在佈局文件中添加:緩存
<com.airbnb.lottie.LottieAnimationView android:id="@+id/animation_view" android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_fileName="hello-world.json" app:lottie_loop="true" app:lottie_autoPlay="true" />
你也能夠選擇使用 Java 代碼的方式進行動畫加載,從app/src/main/assets獲取json文件:網絡
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view); animationView.setAnimation("hello-world.json"); animationView.loop(true);//設置動畫是否循環播放,true表示循環播放,false表示只播放一次 animationView.playAnimation();
這種方式會在後臺進行一次性的異步文件加載和動畫渲染工做 。app
若是你想重複利用一個動畫效果,例如在列表的每一個項目中,或者從一個網絡請求的返回中解析JSONObject對象,你能夠採用以下方式先生成一個Cancellable, 而後進行設置:異步
LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view); ... Cancellable compositionCancellable = LottieComposition.Factory.fromJson(getResources(), jsonObject, new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { animationView.setComposition(composition); animationView.playAnimation(); } }); // Cancel to stop asynchronous loading of composition // compositionCancellable.cancel();
你能夠經過以下方式控制動畫或者添加監聽:
animationView.addAnimatorUpdateListener(//監聽動畫進度 new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // Do something. } }); animationView.playAnimation();//開始動畫 ... animationView.cancelAnimation();//結束動畫 ... animationView.pauseAnimation();//暫停動畫 ... animationView.resumeAnimation();//重啓動畫 ... animationView.setScaleX(0.5f);//設置X軸方向上的縮放比例,0f爲不可見,1f原始大小 Ps.原setScale方法在2.0.0版本後已棄用 animationView.setScaleY(0.5f);//設置Y軸方向上的縮放比例 ... if (animationView.isAnimating()) {//動畫正在進行中 // Do something. } ... animationView.setProgress(0.5f);//手動設置動畫進度 ... // Custom animation speed or duration. ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f)//自定義一個屬性動畫 .setDuration(500); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { animationView.setProgress(animation.getAnimatedValue()); } }); animator.start(); ...
你能夠給整個動畫、一個特定的圖層或者一個圖層的特定內容添加一個顏色過濾器:
// 任何符合顏色過濾界面的類 final PorterDuffColorFilter colorFilter = new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.LIGHTEN); // 在整個視圖中添加一個顏色過濾器 animationView.addColorFilter(colorFilter); //在特定的圖層中添加一個顏色濾鏡 animationView.addColorFilterToLayer("hello_layer", colorFilter); // 添加一個彩色過濾器特效「hello_layer」上的內容 animationView.addColorFilterToContent("hello_layer", "hello", colorFilter); // 清除全部的顏色濾鏡 animationView.clearColorFilters();
你也能夠在佈局文件中爲動畫控件添加一個顏色過濾器:
<com.airbnb.lottie.LottieAnimationView android:layout_width="wrap_content" android:layout_height="wrap_content" app:lottie_fileName="hello-world.json" app:lottie_colorFilter="@color/blue" />
注意:顏色過濾器只適用於圖層,如圖像層和實體層,以及包含填充、描邊或組內容的內容。
在內部, LottieAnimationView 使用 LottieDrawable 做爲其代理的方式呈現其動畫,您甚至能夠直接使用 drawable 表單:
LottieDrawable drawable = new LottieDrawable(); LottieComposition.Factory.fromAssetFileName(getContext(), "hello-world.json", new OnCompositionLoadedListener() { @Override public void onCompositionLoaded(LottieComposition composition) { drawable.setComposition(composition); } });
若是你的動畫會常常重用,LottieAnimationView內置了一個可選的緩存策略。使用LottieAnimationView .setAnimation(String,CacheStrategy)。CacheStrategy能夠爲Strong, Weak, 或者None。LottieAnimationView對加載和解析的動畫持有強引用或弱引用,弱或強表示緩存中組合的回收對象的優先級。
若是你的動畫是從assets中加載的,而且你的圖像文件位於assets 的子目錄中,那麼你能夠對圖像進行動畫處理。你可使用 LottieAnimationView 或者 LottieDrawable 對象調用 setImageAssetsFolder(String) 方法讀取assets目錄中的文件,確保圖像 bodymovin 生成的圖像文件所保存的文件夾以 img_ 開頭。若是直接使用 LottieDrawable, 當你完成時您必須調用 recycleBitmaps方法。
若是你須要提供你本身的位圖,若是你從網絡或其餘地方下載,你能夠提供一個委託來作這個工做:
animationView.setImageAssetDelegate(new ImageAssetDelegate() { @Override public Bitmap fetchBitmap(LottieImageAsset asset) { getBitmap(asset); } });
Lottie使用json文件來做爲動畫數據源,json文件是經過 AE 插件 Bodymovin 導出的,查看sample中給出的json文件,其實就是把圖片中的元素進行來拆分,而且描述每一個元素的動畫執行路徑和執行時間。Lottie的功能就是讀取這些數據,而後繪製到屏幕上。
如今思考若是咱們拿到一份json格式動畫如何展現到屏幕上:首先要解析json,創建數據到對象的映射(LottieComposition),而後根據數據對象建立合適的 Drawable (LottieDrawable)並繪製到 View (LottieAnimationView)上,動畫的實現能夠經過操做讀取到的元素完成,以下圖所示:
在分析映射過程以前,咱們先來看看由 Bodymovin導出的 json 文件的格式:
{ "assets": [ ], "layers": [ { "ddd": 0, "ind": 0, "ty": 1, "nm": "MASTER", "ks": { "o": { "k": 0 }, "r": { "k": 0 }, "p": { "k": [ 164.457, 140.822, 0 ] }, "a": { "k": [ 60, 60, 0 ] }, "s": { "k": [ 100, 100, 100 ] } }, "ao": 0, "sw": 120, "sh": 120, "sc": "#ffffff", "ip": 12, "op": 179, "st": 0, "bm": 0, "sr": 1 }, …… ], "v": "4.4.26", "ddd": 0, "ip": 0, "op": 179, "fr": 30, "w": 325, "h": 202 }
層級很是豐富,除了包含動畫寬、高、幀率等基本屬性外,還包含了重要的的圖層信息layers,以及包含其餘動畫信息的遞歸子集assets。
而後咱們在來觀察 LottieComposition 這個類的結構:
能夠看到startFrame、endFrame、duration、scale等都是動畫中常見的屬性,Factory是靜態內部類(後文會進行分析),剩餘的幾個屬性就值得玩味了:
咱們再看靜態內部類 Factory ,首先從命名上咱們能夠看到他有以下幾個入口:
通過梳理髮現這幾個函數的調用關係以下:
也就是入口函數實際只有這三個:
正是經過這三個入口接收json文件、json流,而後經過AsynTask進行異步處理,最終核心處理都是在 fromJsonSync 中進行json數據的解析。
再來看fromJsonSync函數中的處理過程:
首先獲取動畫區域的寬高:
... int width = json.optInt("w", -1); int height = json.optInt("h", -1); ...
而後根據縮放比例換算實際所須要的寬高:
... int scaledWidth = (int) (width * scale); int scaledHeight = (int) (height * scale); ...
再根據實際寬高獲得一塊矩形區域
... bounds = new Rect(0, 0, scaledWidth, scaledHeight); ...
而後獲取動畫的初始幀,結束幀和幀率,並初始化 LottieComposition對象:
... 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); ...
最後去解析assets 層級中的 LottieImageAsset屬性並存儲在images屬性中,解析assets層級中的Layer屬性並存儲在precomps屬性中,解析外層的Layer屬性並存儲在layers屬性中,返回 LottieComposition 對象:
... JSONArray assetsJson = json.optJSONArray("assets"); parseImages(assetsJson, composition); parsePrecomps(assetsJson, composition); parseLayers(json, composition); return composition; ...
當LottieCompostion 返回後,會回調 LottieAnimationView.setComposition 方法。LottieAnimationView則經過代理屬性--一個LottieDrawable對象,調用其內部的 setComposition 方法:
... 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; } ...
咱們看到 LottieDrawable 中的 setComposition 方法:
/** * @return True if the composition is different from the previously set composition, false otherwise. */ @SuppressWarnings("WeakerAccess") public boolean setComposition(LottieComposition composition) { if (getCallback() == null) { throw new IllegalStateException( "You or your view must set a Drawable.Callback before setting the composition. This " + "gets done automatically when added to an ImageView. " + "Either call ImageView.setImageDrawable() before setComposition() or call " + "setCallback(yourView.getCallback()) first."); } if (this.composition == composition) { return false; } clearComposition(); this.composition = composition; setSpeed(speed); setScale(1f); updateBounds(); buildCompositionLayer(); applyColorFilters(); setProgress(progress); if (playAnimationWhenCompositionAdded) { playAnimationWhenCompositionAdded = false; playAnimation(); } if (reverseAnimationWhenCompositionAdded) { reverseAnimationWhenCompositionAdded = false; reverseAnimation(); } return true; }
能夠看到 LottieDraw 先清理了舊的 compositionLayer 對象,從新創建了對 compostion 對象的引用,設置了 speed、setScale 等屬性,而後經過 buildCompositionLayer 方法從新建立 compostionLayer 對象。
看一看 buildCompositionLayer 方法作了什麼:
private void buildCompositionLayer() { compositionLayer = new CompositionLayer( this, Layer.Factory.newInstance(composition), composition.getLayers(), composition); }
經過 compostion 建立了一個 Layer 對象,並將自身、 compostion 對象中的 layers 屬性及 composition 對象做爲參數初始化了一個 CompositionLayer 對象:
CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels, LottieComposition composition) { super(lottieDrawable, layerModel); LongSparseArray<BaseLayer> layerMap = new LongSparseArray<>(composition.getLayers().size()); BaseLayer mattedLayer = null; for (int i = layerModels.size() - 1; i >= 0; i--) { Layer lm = layerModels.get(i); BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition); if (layer == null) { continue; } layerMap.put(layer.getLayerModel().getId(), layer); if (mattedLayer != null) { mattedLayer.setMatteLayer(layer); mattedLayer = null; } else { layers.add(0, layer); switch (lm.getMatteType()) { case Add: case Invert: mattedLayer = layer; break; } } } for (int i = 0; i < layerMap.size(); i++) { long key = layerMap.keyAt(i); BaseLayer layerView = layerMap.get(key); BaseLayer parentLayer = layerMap.get(layerView.getLayerModel().getParentId()); if (parentLayer != null) { layerView.setParentLayer(parentLayer); } } }
大體就是將lottieDrawable、Layer傳遞給了Parent class, 並將外部 layer 都轉換爲 BaseLayer 並存儲到了一個LongSparseArray中,併爲全部BaseLayer設置了他的父親 BaseLayer屬性。
然而 CompositionLayer 又繼承於 BaseLayer, 咱們來看看它的 draw 方法:
@Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) { if (!visible) { return; } buildParentLayerListIfNeeded(); matrix.reset(); matrix.set(parentMatrix); for (int i = parentLayers.size() - 1; i >= 0; i--) { matrix.preConcat(parentLayers.get(i).transform.getMatrix()); } int alpha = (int) ((parentAlpha / 255f * (float) transform.getOpacity().getValue() / 100f) * 255); if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) { matrix.preConcat(transform.getMatrix()); drawLayer(canvas, matrix, alpha); return; } rect.set(0, 0, 0, 0); getBounds(rect, matrix); intersectBoundsWithMatte(rect, matrix); matrix.preConcat(transform.getMatrix()); intersectBoundsWithMask(rect, matrix); rect.set(0, 0, canvas.getWidth(), canvas.getHeight()); canvas.saveLayer(rect, contentPaint, Canvas.ALL_SAVE_FLAG); // Clear the off screen buffer. This is necessary for some phones. clearCanvas(canvas); drawLayer(canvas, matrix, alpha); if (hasMasksOnThisLayer()) { applyMasks(canvas, matrix); } if (hasMatteOnThisLayer()) { canvas.saveLayer(rect, mattePaint, SAVE_FLAGS); clearCanvas(canvas); //noinspection ConstantConditions matteLayer.draw(canvas, parentMatrix, alpha); canvas.restore(); } canvas.restore(); }
能夠看到 BaseLayer 先繪製了最底層的內容,而後開始繪製包含的 layers 的內容,這個過程相似與界面中的 ViewGroup 嵌套繪製,其中須要用到 drawLayer 來進行layers的繪製,那咱們再回到 CompostionLayer中的 drawLayer方法:
@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { canvas.getClipBounds(originalClipRect); newClipRect.set(0, 0, layerModel.getPreCompWidth(), layerModel.getPreCompHeight()); parentMatrix.mapRect(newClipRect); for (int i = layers.size() - 1; i >= 0 ; i--) { boolean nonEmptyClip = true; if (!newClipRect.isEmpty()) { nonEmptyClip = canvas.clipRect(newClipRect); } if (nonEmptyClip) { layers.get(i).draw(canvas, parentMatrix, parentAlpha); } } if (!originalClipRect.isEmpty()) { canvas.clipRect(originalClipRect, Region.Op.REPLACE); } }
至此,LottieAnimationView的繪製流程結束。