源码浅析-Lottie动画在安卓上是怎么动起来的

lottie是由Airbnb发布的一款针对移动端、PC端、Web端都适用的开源动画库,它是通过AE设计师设计好动画效果,生成json文件,开发者只需要直接将json丢给lottie库,即可实现炫酷动画效果,节省了很多开发工作量。

近期工作正好涉及到lottie动画相关的开发,于是clone了一份源码进行学习分析,这里对源码进行分析实现总结(针对安卓端)

源码地址看这里:airbnb/lottie-android

基本使用:

  • 通过布局方式,自动播放hello_world动画

  • 通过LottieAnimationView,控制播放动画
      LottieAnimationView lottieAnimationView = findViewById(R.id.lottieAnimationView);
      lottieAnimationView.setAnimation("hello_world.json");
      lottieAnimationView.playAnimation();
      //...
  • 通过LottieDrawable,设置到View上,也可控制播放动画
        ImageView ivLottie = findViewById(R.id.iv_lottie);
        LottieDrawable lottieDrawable = new LottieDrawable();
        LottieCompositionFactory.fromAsset(this, "AndroidWave.json").addListener(result -> {
            lottieDrawable.setComposition(result);
            lottieDrawable.playAnimation();
        });
        ivLottie.setImageDrawable(lottieDrawable);

主要实现类

Lottie源码目录结构如下:


1660877087431.png

通过上面使用示例可以看出,LottieAnimationView是对开发者暴露的封装,核心的动画逻辑配置实现是在LottieDrawable进行的。

分析其几个主要实现类:

LottieAnimationView
继承自ImageView
封装动画操作设置接口,由接入方直接在布局文件中或代码中使用
LottieDrawable
lottie动画绘制的具体实现,替代LottieAnimationView的父类ImageView的drawable。
compositionLayer
继承自抽象类BaseLayer
封装json动画的图层的具体绘制过程,主要用于绘制特定图层以及子图层
LottieValueAnimator
继承自ValueAnimator
通过控制属性动画的具体实现
LottieCompositionFactory
LottieComposition的解析工厂,提供一系列解析lottie动画资源的方法,最终解析json生成LottieComposition
LottieImageAssets
lottie图片动画资源中的图片信息的数据模型,包括基本的宽高、对应图片、id等信息,也是图片bitmap化后的存储对象。
LottieTask
内部维护一个ThreadExcutor,用于管理lottie动画解析的task,对外抛出相应事件。
LottieDrawable
lottie动画绘制的具体实现,替代LottieAnimationView的父类ImageView的drawable。
ImageAssetManager
针对带图片的lottie动画图片资源管理类,图片最终以bitmap形式存储在hashmap中,且通过强引用方式,所以内存占用较大。

LottieAndroid动画驱动绘制流程

阅读安卓端源码可以看出,Lottie动画运行驱动是基于ValueAnimator,动画数据源基于从lottie json数据解析的结果Composition对象,ValueAnimator通过不断对外抛出valueChange事件,来让动画的layer不断完成绘制,并将绘制内容抛出到Drawable上,再通过View渲染,从而实现json数据到动画动起来的效果。

这里根据源码实现,绘制了LottieAndroid主流程的时序图:

LottieAndroid主流程时序图.jpg

优劣势:

  • 节省开发成本,这点毋庸置疑,省去了动画实现的复杂开发过程。
  • 复杂动画实现更高效,复杂动画在安卓端目前还没有最优解决方案,lottie应该算是靠前了。
  • 内存占用高,当播放带图片资源的lottie动画时,源码中直接将图片转化为强引用Bitmap存在内存,没有做优化处理,如果图片资源过多,容易OOM。
  • Lottie 动画资源设计导出需要设计师有一定的经验,在我工作中对接的设计师,导出的动画好几次存在漏图的问题,增加了沟通成本。
  • ...

主流程部分源码

lottie通过先设置,后启动的方式播放动画,设置的属性用于控制动画的播放状态, 我们可以从lottie关联到json动画的源码处入手:

lottieAnimationView.setAnimation("hello_world.json");


class LottieAnimationView{
//LottieAnimationView
private final LottieListener loadedListener = this::setComposition;
  /**
   * Sets a composition.
   * You can set a default cache strategy if this view was inflated with xml by
   * using {@link R.attr#lottie_cacheComposition}.
   */
  public void setComposition(@NonNull LottieComposition composition) {
//...
boolean isNewComposition = lottieDrawable.setComposition(composition);
//...
      setLottieDrawable();
//...
    requestLayout();
//...
  }
  public void setAnimation(final String assetName) {
    this.animationName = assetName;
    animationResId = 0;
    setCompositionTask(fromAssets(assetName));
  }

  private void setCompositionTask(LottieTask compositionTask) {
    userActionsTaken.add(UserActionTaken.SET_ANIMATION);
    clearComposition();
    cancelLoaderTask();
    this.compositionTask = compositionTask
        .addListener(loadedListener)
        .addFailureListener(wrappedFailureListener);
  }

}

//LottieTask
public class LottieTask {
  //...
  public static Executor EXECUTOR = Executors.newCachedThreadPool();
}
  @RestrictTo(RestrictTo.Scope.LIBRARY)
  public LottieTask(Callable> runnable) {
    this(runnable, false);
  }

  /**
   * runNow is only used for testing.
   */
  @RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Callable> runnable, boolean runNow) {
    if (runNow) {
      try {
        setResult(runnable.call());
      } catch (Throwable e) {
        setResult(new LottieResult<>(e));
      }
    } else {
      EXECUTOR.execute(new LottieFutureTask(runnable));
    }
  }
//...

可以看出,在设置动画资源时,将json文件的解析过程封装成了LottieTask的形式,查看LottieTask实现,可以看出,其内部维护了线程池,并且其构造函数也可以看出,可以在初始化时,立即将解析任务放入线程池。在得到解析结果后,又会回调LottieAnimationView的setComposition方法,设置Composition。

前面已经知道,Composition对象就是json动画文件的代码化表达方式。LottieAndroid做的就是将Composition消费成动画。

这里,在LottieAnimationView的setComposition方法中,可以看到调用了LottieDrawable的setComposition方法,同时下一步通过setDrawable将drawable与LottieAnimationView关联。那么,就看下LottieDrawable的实现。

  public class LottieDrawable{
      ArrayList lazyCompositionTasks = new ArrayList<>();
      LottieValueAnimator animator = new LottieValueAnimator();
       public boolean setComposition(LottieComposition composition) {
          //...  
            buildCompositionLayer() 
        }
  private void buildCompositionLayer() {
    LottieComposition composition = this.composition;
    if (composition == null) {
      return;
    }
    compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);
    if (outlineMasksAndMattes) {
      compositionLayer.setOutlineMasksAndMattes(true);
    }
    compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
  }

  @MainThread
  public void playAnimation() {

//...
        animator.playAnimation();
        onVisibleAction = OnVisibleAction.NONE;
// ...
  }

}

LottieDrawable负责具体的动画实现,这里初始化动画数据就是setComposition函数做的事,主要是需要根据LottieComposition 构建出图层数据(CompositionLayer),Layer在json数据中也可以看出这个节点,lottie的动画绘制主要是递归Layer进行draw。

图层数据有了之后,就算是动画数据准备好了,直接调用playAnimation,即可触发动画播放了。playAnimation触发了LottieValueAnimation的playAnimation。

public abstract class BaseLottieAnimator extends ValueAnimator {
  void notifyUpdate() {
    for (AnimatorUpdateListener listener : updateListeners) {
      listener.onAnimationUpdate(this);
    }
  }
}

LottieAnimator 在运行过程中,不断对外抛出onAnimationUpdate事件。

public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
    private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
      if (compositionLayer != null) {
        compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
      }
    }
  };
}

LottieDrawable 接收到onAnimationUpdate事件,又将其事件值转化为progress,抛给compostionLayer:

public class CompositionLayer extends BaseLayer {
    @Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress);
//...
    if (mask != null) {
      for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
        mask.getMaskAnimations().get(i).setProgress(progress);
      }
    }
    if (inOutAnimation != null) {
      inOutAnimation.setProgress(progress);
    }
    if (matteLayer != null) {
      matteLayer.setProgress(progress);
    }
    for (int i = 0; i < animations.size(); i++) {
      animations.get(i).setProgress(progress);
    }
    // ...
    for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
  }

}

ComposeLayer又将progresschange事件抛给动画子元素,层层判断是否需要处理progress,类似点击事件传递。

public abstract class BaseKeyframeAnimation {
    final List listeners = new ArrayList<>(1);
    public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
     // ...
    if (keyframesWrapper.isValueChanged(progress)) {
      notifyListeners();
    }
  }

  public void notifyListeners() {
    for (int i = 0; i < listeners.size(); i++) {
      listeners.get(i).onValueChanged();
    }
  }
}

其中的一个动画帧元素消费progress的逻辑,可以看出,这里又对外抛出onValueChanged事件。跟踪代码,onValueChanged接收者消费这个事件的逻辑都是一致的,最终调用的都是:

  class BaseLayer  implements DrawingContent, BaseKeyframeAnimation.AnimationListener, KeyPathElement {
      @Override
  public void onValueChanged() {
    invalidateSelf();
  }

  private void invalidateSelf() {
    lottieDrawable.invalidateSelf();
  }
  }

根据View的绘制逻辑,会由LottieDrawable执行绘制,也就是会调用LottieDrawable的draw方法。LottieDrawable的draw又会调用compositionLayer的draw方法。

public class LottieDrawable extends Drawable implements Drawable.Callback, Animatable {
     public void draw(@NonNull Canvas canvas) {
      //...
        compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
      //...
    }

}

最终,在CompositionLayer中完成了当前动画帧的绘制,循环往复,也就呈现了动画效果。

你可能感兴趣的:(源码浅析-Lottie动画在安卓上是怎么动起来的)