酷炫的动画库——Lottie源码解析(一)

2,724 阅读7分钟

图片来自必应

简介:Lottie是可以将AE做出的动画,通过Bodymovin 输出为一个json文件,然后Lottie可以将这个json文件渲染为本地动画,包括Android、iOS、React native、还包括PC端也可以使用。使用LottieView,我们就可以很方便的实现一些复杂的动画,而且也不需要各端自己实现,或者说出现各端实现出来之后效果不一样的情况。如果使用LottieView的话,我们仅仅需要设计师输出一份json文件就可以了。

Lottie官方文档

首先看一下LottieView官方网站上的一些动画的效果:

可以看出这些动画都有很酷炫的效果,如果要通过代码去实现的话就会非常的复杂,但是如果使用Lottie去加载,则非常简单,只需要几行代码就可以搞定,而且帧率大多都会稳定在60FPS。

基本使用

使用方式很简单,只需要在xml文件中声明一个LottieAnimationView ( 当然它还有一些属性可以在xml中设置,这里不再赘述,属性名称都能看的出来其作用),然后在代码中设置:

LottieCompositionFactory.fromAsset(context, "assertName").addListener{
  lottieView.setComposition(it)
  lottieView.playAnimation()
}.addFailureListener{
  //Load Error
}

只需要上面这样几行代码,就可以实现复杂的动画了。可以看出首先是去assert中加载了一个json动画的资源,加载成功后会调用Listener,给LottieView设置Composition,然后调用playAnimation() 方法播放就可以了,至于Composition 是什么,我们后面会说到。

源码解析

我们从 lottieView.playAnimation() 开始,因为它是开始播放的方法。那么跟踪源码就能看到:

 /**
   * Plays the animation from the beginning. If speed is < 0, it will start at the end
   * and play towards the beginning
   */
  @MainThread
  public void playAnimation() {
    if (isShown()) {
      lottieDrawable.playAnimation();
      enableOrDisableHardwareLayer(); 
    } else {
      wasAnimatingWhenNotShown = true;
    }
  }

可以看到,在 playAnimation() 中,首先判断了 isShown ,也就是当前的LottieView是否显示,然后会调用 lottieDrawable.playAnimation(); 那么 lottieDrawable 是什么呢?因为LottieView都是继承自ImageView,所以我们需要给其指定一个资源,那么这个 lottieDrawable 就是要指定的资源,而LottieAnimationView实际上只是起到了一个容器的作用。所以,Lottie动画实现的关键实际上是在这个LottieDrawable中,这个之后再看。

再看第二句,enableOrDisableHardwareLayer() ,该方法是做了是否开启硬件的判断,主要是根据动画解析后的一些规则,例如是mask和matte是否超过4个,以及系统的版本。这是因为在一些场景下,开启硬件加速的表现实际上并不如软件渲染,因此只有当满足一些条件后才会开启。(当然也可以自己设定)

接下来看一下lottieDrawable.playAnimation() 这个方法做了什么:

 @MainThread
  public void playAnimation() {
    if (compositionLayer == null) {
      lazyCompositionTasks.add(new LazyCompositionTask() {
        @Override
        public void run(LottieComposition composition) {
          playAnimation();
        }
      });
      return;
    }

    if (systemAnimationsEnabled || getRepeatCount() == 0) {
      animator.playAnimation();
    }
    if (!systemAnimationsEnabled) {
      setFrame((int) (getSpeed() < 0 ? getMinFrame() : getMaxFrame()));
    }
  }

上面的代码首先是判断了 compositionLayer 是否为空,如果为空的话,就将playAnimation()放进一个延迟执行的任务队列中,然后会调用 animator.playAnimation(); **关于compositionLayer是什么,我们先带着这个疑问看下去。**这里又调用到了LottieValueAnimator的playAnimation:

public void playAnimation() {
    running = true;
    notifyStart(isReversed());
    setFrame((int) (isReversed() ? getMaxFrame(·) : getMinFrame()));
    lastFrameTimeNs = 0;
    repeatCount = 0;
    postFrameCallback();
}

可以看出,这个方法中调用了三个重要的方法:notifyStart(isReversed())、 setFrame()、和postFrameCallback(). 其中notifyStart主要是开始动画,然后看一下setFrame都做了什么:

 public void setFrame(float frame) {
    if (this.frame == frame) {
      return;
    }
    this.frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
    lastFrameTimeNs = 0;
    notifyUpdate();
  }

// notifyUpdate()
void notifyUpdate() {
    for (ValueAnimator.AnimatorUpdateListener listener : updateListeners) {
      listener.onAnimationUpdate(this);
    }
}

在notifyUpdate中,遍历了当前Lottie的所有动画,并且调用了onAnmationUpdate()方法,这个方法的实现,是在LottieDrawable中:

private final ValueAnimator.AnimatorUpdateListener  progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
      if (compositionLayer != null) {
        compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
      }
    }
  };

最终是调用了compositionLayer的setProgress方法:

@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    super.setProgress(progress);
    if (timeRemapping != null) {
      float duration = lottieDrawable.getComposition().getDuration();
      long remappedTime = (long) (timeRemapping.getValue() * 1000);
      progress = remappedTime / duration;
    }
    if (layerModel.getTimeStretch() != 0) {
      progress /= layerModel.getTimeStretch();
    }

    progress -= layerModel.getStartProgress();
    for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
  }

在这个方法中,计算了当前的progress,然后计算了当前的duration,然后获取了所有的layers,调用了每一个layer的setProgress方法,跟进去之后发现,是调用的BaseLayer的setProgress方法:

void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    // Time stretch should not be applied to the layer transform.
    transform.setProgress(progress);
    if (mask != null) {
      for (int i = 0; i < mask.getMaskAnimations().size(); i++) {
        mask.getMaskAnimations().get(i).setProgress(progress);
      }
    }
    if (layerModel.getTimeStretch() != 0) {
      progress /= layerModel.getTimeStretch();
    }
    if (matteLayer != null) {
      // The matte layer's time stretch is pre-calculated.
      float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
      matteLayer.setProgress(progress * matteTimeStretch);
    }
    for (int i = 0; i < animations.size(); i++) {
      animations.get(i).setProgress(progress);
    }
  }

又调用了animations的setProgress,

public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
    if (keyframes.isEmpty()) {
      return;
    }
    // Must use hashCode() since the actual object instance will be returned
    // from getValue() below with the new values.
    Keyframe<K> previousKeyframe = getCurrentKeyframe();
    if (progress < getStartDelayProgress()) {
      progress = getStartDelayProgress();
    } else if (progress > getEndProgress()) {
      progress = getEndProgress();
    }

    if (progress == this.progress) {
      return;
    }
    this.progress = progress;
    // Just trigger a change but don't compute values if there is a value callback.
    Keyframe<K> newKeyframe = getCurrentKeyframe();

    if (previousKeyframe != newKeyframe || !newKeyframe.isStatic()) {
      notifyListeners();
    }
  }

这里比较关键, 首先根据之前的progress获取到了上一帧,也就是keyFrame,keyFrame里面存储了一些坐标信息,以及一些坐标点,通过对它的值的改变,达到动画中,View的变化:

public class Keyframe<T> {
  private static final float UNSET_FLOAT = -3987645.78543923f;
  private static final int UNSET_INT = 784923401;

  @Nullable private final LottieComposition composition;
  @Nullable public final T startValue;
  @Nullable public T endValue;
  @Nullable public final Interpolator interpolator;
  public final float startFrame;
  @Nullable public Float endFrame;

  private float startValueFloat = UNSET_FLOAT;
  private float endValueFloat = UNSET_FLOAT;

  private int startValueInt = UNSET_INT;
  private int endValueInt = UNSET_INT;

  private float startProgress = Float.MIN_VALUE;
  private float endProgress = Float.MIN_VALUE;

  // Used by PathKeyframe but it has to be parsed by KeyFrame because we use a JsonReader to
  // deserialzie the data so we have to parse everything in order
  public PointF pathCp1 = null;
  public PointF pathCp2 = null;
}

获取了上一个keyFrame 之后,会更新progress,根据新的progress获取到新的一帧,然后当两帧不相等的时候,调用notifyListeners:

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

调用了onValueChanged,在动画的每一个图层都有实现,最终会调用到lottieDrawable.invalidateSelf()。 这里先强调一个图层的概念,之后回说到。然后这个invalidateSelf() 会调用 LottieAnimationView的invalidateDrawable()方法:

@Override public void invalidateDrawable(@NonNull Drawable dr) {
    if (getDrawable() == lottieDrawable) {
      // We always want to invalidate the root drawable so it redraws the whole drawable.
      // Eventually it would be great to be able to invalidate just the changed region.
      super.invalidateDrawable(lottieDrawable);
    } else {
      // Otherwise work as regular ImageView
      super.invalidateDrawable(dr);
    }
  }

这里调用了父类ImageView的invalidateDrawable方法,刷新了ImageView的内容。

以上就是LottieAnimationView的playAnimation的整个过程。

下面给出一个整体的时序图,序号表明了调用顺序:

以上就是LottieAnimationView的playAnimation的整个过程。那么会有一个问题,动画是哪里来的,Lottie如何完成从一个json文件到实现动画的?

实际上,关键就在于 lottieView.setComposition(composition) 这一句代码上,参数composition是解析json动画文件后返回的,在 lottieAnimationView.setComposition() 中,又调用了 lottieDrawable.setComposition(),看一下这个方法的实现:

public boolean setComposition(LottieComposition composition) {
    if (this.composition == composition) {
      return false;
    }

    isDirty = false;
    clearComposition();
    this.composition = composition;
    buildCompositionLayer();
    animator.setComposition(composition);
    setProgress(animator.getAnimatedFraction());
    setScale(scale);
    updateBounds();

    // We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
    // then there won't be two iterators iterating and removing at the same time.
    Iterator<LazyCompositionTask> it = new ArrayList<>(lazyCompositionTasks).iterator();
    while (it.hasNext()) {
      LazyCompositionTask t = it.next();
      t.run(composition);
      it.remove();
    }
    lazyCompositionTasks.clear();

    composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);

    return true;
  }

这段代码中有两个关键的部分,一个是buildCompositionLayer() 方法, 还有一个可以看一下while循环内部,我们前面在看playAnimation的源码的时候,当composition为null时,会把当前要执行的动作添加进一个lazyCompositionTask中,它就是在这里执行的,因为这时候已经给composition赋值了。然后看一下buildCompositionLayer()的源码:

private void buildCompositionLayer() {
    compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);
  }

可以看到这里初始化了一个CompositionLayer,用的是composition这个对象,先了解一下CompositionLayer是什么:实际上CompositionLayer就是一个布局的图层,它内部有一个drawLayer方法:

@Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
    L.beginSection("CompositionLayer#draw");
    canvas.save();
    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) {
        BaseLayer layer = layers.get(i);
        layer.draw(canvas, parentMatrix, parentAlpha);
      }
    }
    canvas.restore();
    L.endSection("CompositionLayer#draw");
  }

从这个方法可以看出,它是包含了一个layer(图层)的集合layers,然后调用了每一个layer的draw方法。从这里我们就能初步发现,实际上Lottie的动画就是由一个一个的layer组成的,通过一层一层的layer,最终实现了Lottie动画,我们这里先给出一个总结,在下一章会详细分析动画的组成,以及动画json文件的分析。