lottie

概念

Lottie 是 Airbnb開源的一套跨平臺的完整的動畫效果解決方案,它可以使用Bodymovin解析以json導出的Adobe After Effects動畫,并在移動設備上進行本地渲染。
可以直接運用在 iOS、Android、WebReact 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多端通用
  • 效果
    幾乎與設計出的動畫無差別

使用流程

image.png

使用方法

由于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文件結構

json格式3.png
  • 回憶: 幀動畫的播放.

核心類

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

解析流程.jpg
類結構.png

動畫播放原理

    #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();
  }

動畫播放時序圖.jpg

動畫適配原理

① 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的導出.
且通過配置使得動畫過程中圖片都可以得到復用。

  1. 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(jié)省了CPU
  2. 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的導出.

  1. 由于擁有所有幀, 不用解析高階插值(二次線性方程,貝塞爾曲線方程),節(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

設計師工具
http://svga.io/designer.html

動畫預覽器
http://svga.io/svga-preview.html

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容