Lottie动画源码解析

Lottie是一个很成熟的开源动画框架,它支持直接使用从AE导出的动画文件,在不同平台均可快速使用,大大减轻了程序员的工作量,也让复杂的动画成为可能。该动画文件使用Json格式来描述内容,可以大大缩减文件的体积。在Android平台,Lottie的作用就是把Json文件做解析,使用code做控制,并通过Canvas绘制出来。Lottie效果如下:
如果用纯代码去手搓以上的动画效果,估计码农们会原地狗带。而用Lottie作为衔接,这一切就很丝滑,就像显示一张图片一样简单,这就是Lottie的强大,可以说是降维打击。

1.文件结构

1.1最外层文件

Json的最外层结构如下:

java 复制代码
{
  "v": "5.8.0",  //bodymovin的版本
  "fr": 60,      //帧率
  "ip": 0,       //起始关键帧
  "op": 102,     //结束关键帧
  "w": 1350,     //动画宽度
  "h": 800,      //动画高度
  "nm": "recommend_turn page_x0.75_original", //名称
  "ddd": 0,       //是否为3d
  "assets":[],   //资源信息
  "layers":[],   //图层信息
  "markers": []  //遮罩
}

其中的layers是动画最核心的内容,因为Lottie的动画是由不同的Layer组合而成的。

1.2 Layers层

结构如下:

java 复制代码
"layers": [                            //图层信息
    {
      "ddd": 0,         //是否为3d
      "ind": 1,                     //图层id 唯一性
      "ty": 4,            //图层类型
      "nm": "page back 4",//图层名称
      "refId": "comp_0", // 引用的资源,图片/预合成层
      "td": 1,
      "sr": 1,
      "ks": {...},              // 变换。对应AE中的变换设置
      "ao": 0,
      "layer": [],         // 该图层包含的子图层
      "shaps": [],         // 形状图层
      "ip": 12,                     //该图层起始关键帧
      "op": 1782,         //该图层结束关键帧
      "st": -18,         
      "bm": 0
    }

ty : 定义了图层的类型,类型有ImageLayer、ShapeLayer、ScaleLayer、SolidLayer、TextLayer和NullLayer,不同的layer会使用不同的变化策略以及不同的资源。
refId :这是ImageLayer会用到的图片资源。
shaps :ShapeLayer会用到的资源,描述形状。
ks :变换的描述,所有图层都会用到这个资源,它是对动画怎么变化的具体描述,它内部包含了很多维度的变。

1.3 KS层

具体如下:

java 复制代码
"ks": { // 变换。对应AE中的变换设置
    "o": { // 透明度
        "a": 0,
        "k": 100,
        "ix": 11
    },
    "r": { // 旋转
        "a": 0,
        "k": 0,
        "ix": 10
    },
    "p": { // 位置
        "a": 0,
        "k": [-167, 358.125, 0],
        "ix": 2
    },
    "a": { // 锚点
        "a": 0,
        "k": [667, 375, 0],
        "ix": 1
    },
    "s": { // 缩放
        "a": 0,
        "k": [100, 100, 100],
        "ix": 6
    }
}

2.文件解析过程

2.1 LottieTask

在LottieAnimationView的setAnimation方法是加载资源的入口,它内部会把加载资源的任务创建一个LottieTask,并且在执行完文件解析之后,创建相关的Layer,并封装在LottieComposition作为结果返回给LottieAnimationView,如下:

java 复制代码
public void setAnimation(@RawRes final int rawRes) {
    LottieTask<LottieComposition> task = cacheComposition ?
        LottieCompositionFactory.fromRawRes(getContext(), rawRes) : LottieCompositionFactory.fromRawRes(getContext(), rawRes, null);
    //添加任务监听,在任务执行成功之后回调
   setCompositionTask(task); 
  }

LottieCompositionFactory内部会创建一个LottieTask并添加到线程池中去异步执行:

java 复制代码
public static LottieTask<LottieComposition> fromRawRes(Context context, @RawRes final int rawRes, @Nullable String cacheKey) {
    return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
      @Override
      public LottieResult<LottieComposition> call() {
        // 这里执行异步解析任务
        return fromRawResSync(context, rawRes);
      }
    });
  }

在LottieTask内部中添加线程池任务:

java 复制代码
LottieTask(Callable<LottieResult<T>> runnable, boolean runNow) {
      EXECUTOR.execute(new LottieFutureTask(runnable));
  }

2.2 LayerParser

跟进fromRawResSync方法里面去,会调用到LottieCompositionMoshiParser去做解析最外层的Json数据,我们主要关注是解析layers字段:

LottieCompositionMoshiParser.java

java 复制代码
public static LottieComposition parse(JsonReader reader) throws IOException {
	switch (reader.selectName(NAMES)) {
		case 6:
          parseLayers(reader, composition, layers, layerMap);
	}
}

private static void parseLayers(JsonReader reader, LottieComposition composition,
                                  List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
      Layer layer = LayerParser.parse(reader, composition);
      layers.add(layer);
      layerMap.put(layer.getId(), layer);
  }

最后再调用了LayerParser做生成各个Layer数据结构,当然LayerParser内部还要对下一层的数据做解析,最后形成一个树形数据结构返回给LottieAnimationView。

3.创建绘制图层

3.1 CompositionLayer和BaseLayer

LottieTask在解析完之后,会把LottieComposition返回给LottieAnimationView:

LottieAnimationView.java

java 复制代码
public void setComposition(@NonNull LottieComposition composition) {
    boolean isNewComposition = lottieDrawable.setComposition(composition);
    }

LottieAnimationView又把数据透传给LottieDrawable:

LottieDrawable.java

java 复制代码
public boolean setComposition(LottieComposition composition) {
	buildCompositionLayer();
}

compositionLayer = new CompositionLayer(
        this, LayerParser.parse(composition), composition.getLayers(), composition);

这里会创建一个CompositionLayer,它继承自BaseLayer,而在它内部会创建各个类型的图层,如下:

java 复制代码
 static BaseLayer forModel(
      Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
    switch (layerModel.getLayerType()) {
      case SHAPE:
        return new ShapeLayer(drawable, layerModel);
      case PRE_COMP:
        return new CompositionLayer(drawable, layerModel,
            composition.getPrecomps(layerModel.getRefId()), composition);
      case SOLID:
        return new SolidLayer(drawable, layerModel);
      case IMAGE:
        return new ImageLayer(drawable, layerModel);
      case NULL:
        return new NullLayer(drawable, layerModel);
      case TEXT:
        return new TextLayer(drawable, layerModel);

    }
  }

这些不同类型的Layer也是继承自BaseLayer,所以这里创建图层的过程,就是把之前Json文件解析的树形数据结构,转换成绘制图层的树形结构。CompositionLayer和其他各个BaseLayer的关系,就类似于View和ViewGroup之间的关系一样。

3.2 关键帧动画

同时在创建各个BaseLayer的时候,它们内部还会继续创建对应的关键帧动画类,我们重点看一下TransformKeyframeAnimation,也就是上面Json文件中对应的"ks"数据:

TransformKeyframeAnimation.java

java 复制代码
  @NonNull private BaseKeyframeAnimation<PointF, PointF> anchorPoint;
  @NonNull private BaseKeyframeAnimation<?, PointF> position;
  @NonNull private BaseKeyframeAnimation<ScaleXY, ScaleXY> scale;
  @NonNull private BaseKeyframeAnimation<Float, Float> rotation;
  @NonNull private BaseKeyframeAnimation<Integer, Integer> opacity;
  @Nullable private FloatKeyframeAnimation skew;
  @Nullable private FloatKeyframeAnimation skewAngle;

可以看到内部定义了很多关键帧动画类,它们决定了该Layer在某个时刻应该绘制怎样的内容。

关系图如下:

4.动画绘制过程

4.1LottieValueAnimator计算progress

准备好了资源,创建好了图层,在LottieAnimationView 调用了playAnimation()方法之后,就开始播放动画了。同样,也会调用到LottieDrawable的playAnimation()方法:

LottieDrawable.java

java 复制代码
public void playAnimation() {
	//调用animator去播放动画
     animator.playAnimation();   
  }

LottieDrawable会调用它内部的LottieValueAnimator去计算动画播放的具体帧:

LottieValueAnimator.java

java 复制代码
 @MainThread
  public void playAnimation() {  
  	//这里会通知更新动画
    setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
    //这里注册Choreographer,以便同步vsync信号,并在doframe方法中继续更新动画
    postFrameCallback();
  }

setFrame会调用到父类里的notifyUpdate()方法,然后回调通知到LottieDrawable里面的监听接口:

LottieDrawable.java

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

从这里可以看出,LottieValueAnimator的作用主要是根据屏幕的刷新信号,来计算应该播放的帧序列,也就是播放的具体进度。当存在卡断的情况下,Choreographer的doframe方法会延迟通知,就会导致LottieValueAnimator存在跳帧的情况。

4.2关键帧动画设置progress

接着看绘制流程,播放进度会给到CompositionLayer,而CompositionLayer又会把播放进度再分发给具体的每一个Layer,如下:

CompositionLayer.java

java 复制代码
 setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
	for (int i = layers.size() - 1; i >= 0; i--) {
      layers.get(i).setProgress(progress);
    }
}

每个Layer都是继承自BaseLayer,在BaseLayer中又会把进度给到内部的各个关键帧动画,最重要的是TransformKeyframeAnimation,它内部包含各个维度的变化序列:

TransformKeyframeAnimation.java

java 复制代码
setProgress(float progress) {
if (opacity != null) {
      opacity.setProgress(progress);  //透明度
    }
    if (anchorPoint != null) {
      anchorPoint.setProgress(progress); //锚点
    }
    if (position != null) {
      position.setProgress(progress);  //位置
    }
    if (scale != null) {
      scale.setProgress(progress);  //缩放
    }
    if (rotation != null) {
      rotation.setProgress(progress); //旋转
    }
    if (skewAngle != null) {
      skewAngle.setProgress(progress); //斜角
}

4.3Layer请求刷新

在设置完progress之后,各个Layer就会触发onValueChanged()方法,并请求刷新自己:

BaseLayer.java

java 复制代码
public void onValueChanged() {
    invalidateSelf();
  }

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

接着会调用到LottieDrawable的绘制:

LottieDrawable.java

java 复制代码
public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
      callback.invalidateDrawable(this);
    }
  }

4.4Layer绘制

当自定义的Drawable需要重绘的时候,就需要调用invalidateDrawable方法,请求绘制自己,然后会通过ImageView的onDraw方法里调用到Drawable的draw方法:

Drawable.java

java 复制代码
 public void draw(@NonNull Canvas canvas) {
        drawInternal(canvas);
  }

接下来就会调用到CompositionLayer中的draw方法,在CompositionLayer中又会把实际绘制的工作交给各个Layer去完成:

CompositionLayer.java

java 复制代码
 draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	drawLayer(canvas, matrix, alpha);
}

drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	for (int i = layers.size() - 1; i >= 0; i--) {
		  BaseLayer layer = layers.get(i);
          layer.draw(canvas, parentMatrix, childAlpha);
    	}
}

不同的Layer层,对draw的实现也不一样,比如ImageLayer是drawBitmap,绘制前会在父类调关键帧动画计算出来的参数,对Bitmap做位移、旋转、透明度变化等等变化操作,最后才是使用Canvas做绘制:

BaseLayer.java:

java 复制代码
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	 matrix.preConcat(transform.getMatrix()); //通过关键帧动画做变化操作
	 drawLayer(canvas, matrix, alpha); //调用子类的绘制
}

ImageLayer.java

java 复制代码
 @Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) {
	Bitmap bitmap = getBitmap(); //读取图片内容
	canvas.drawBitmap(bitmap, src, dst , paint);//绘制图片
}

至此,一帧动画的绘制就完成了,后面会由LottieValueAnimator的doFrame回调不停地重复上面的步骤,实现连续的动画效果。

相关推荐
@OuYang6 小时前
android10 audio音量曲线
android
三爷麋了鹿10 小时前
VNC Viewer安卓版安装与操作
android
起个随便的昵称12 小时前
安卓入门十一 常用网络协议四
android·网络
BoomHe12 小时前
Android 车载性能优化-内存泄漏
android
起个随便的昵称13 小时前
安卓入门十三 常用功能模块一RxJava
android
工程师老罗13 小时前
Android笔试面试题AI答之Android基础(11)
android
叶羽西13 小时前
Android Camera压力测试工具
android
java_t_t13 小时前
安卓触摸事件的传递
android·java
千里马学框架15 小时前
千里马2024年终总结-android framework实战
android·framework·input·车机车载
tmacfrank16 小时前
Kotlin 协程基础知识总结五 —— 通道、多路复用、并发安全
android·开发语言·kotlin