Lottie的基本用法及原理分析

一. Lottie能作什麼

回答這個問題前,咱們先想一想下面這些動畫須要怎麼實現:java

              

             

               

是否是一臉懵逼,若是不懵逼是否是感受壓力山大?傳統方式實現動畫,無非如下幾種方式:android

  • 使用Animation/Animator。這種方式當然可行,可是要麼須要添加多張圖片,要麼須要針對單個控件寫一大堆動畫,不管是從apk體積方面考慮仍是從開發效率上來講都得不償失勢;
  • 使用 GIF。一樣面臨的問題是所佔體積較大,並且須要爲各類屏幕尺寸、分辨率作適配,而且Android本是不支持GIF直接展現的;
  • Android 5.0 Lollipop 以後提供了對 SVG 的支持,經過 VectorDrawable、AnimatedVectorDrawable 的結合能夠實現一些稍微複雜的動畫,可是一樣面臨上述問題。

那麼有什麼方法既能夠高效的實現動畫,又不須要佔用過多空間,還能同時支持多個系統環境呢?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

 

二. 使用過程

1.添加依賴

現有版本已升級到2.0.0-rc1canvas

dependencies {  
        compile 'com.airbnb.android:lottie:2.0.0-rc1'
    }

2. 使用

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對加載和解析的動畫持有強引用或弱引用,弱或強表示緩存中組合的回收對象的優先級。

 

三. Image 支持

若是你的動畫是從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)上,動畫的實現能夠經過操做讀取到的元素完成,以下圖所示:

              

1. json文件到對象的映射

在分析映射過程以前,咱們先來看看由 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是靜態內部類(後文會進行分析),剩餘的幾個屬性就值得玩味了:

  • precomps:存儲assets遞歸子集的 Layer 類型屬性的HashMap集合;
  • images:存儲assets遞歸子集中的 LottieImageAsset 類型屬性的HashMap集合;
  • layerMap:存儲外層layers中的Layer類型屬性的LongSparseArray集合。

咱們再看靜態內部類 Factory ,首先從命名上咱們能夠看到他有以下幾個入口:

              

通過梳理髮現這幾個函數的調用關係以下:

             

也就是入口函數實際只有這三個:

  • fromAssetFileName(Context context, String fileName, OnCompositionLoadedListener loadedListener);
  • fromFileSync(Context context, String fileName);
  • fromJson(Resources res, JSONObject json, OnCompositionLoadedListener loadedListener)。

正是經過這三個入口接收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;
      ...

2. 根據對象建立drawable並繪製到View上

當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的繪製流程結束。

相關文章
相關標籤/搜索