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回调不停地重复上面的步骤,实现连续的动画效果。

相关推荐
练习本16 分钟前
Android系统架构模式分析
android·java·架构·系统架构
每次的天空5 小时前
Kotlin 内联函数深度解析:从源码到实践优化
android·开发语言·kotlin
练习本5 小时前
Android MVC架构的现代化改造:构建清晰单向数据流
android·架构·mvc
早上好啊! 树哥6 小时前
android studio开发:设置屏幕朝向为竖屏,强制应用的包体始终以竖屏(纵向)展示
android·ide·android studio
YY_pdd6 小时前
使用go开发安卓程序
android·golang
Android 小码峰啊8 小时前
Android Compose 框架物理动画之捕捉动画深入剖析(29)
android·spring
bubiyoushang8888 小时前
深入探索Laravel框架中的Blade模板引擎
android·android studio·laravel
cyy2988 小时前
android 记录应用内存
android·linux·运维
CYRUS STUDIO9 小时前
adb 实用命令汇总
android·adb·命令模式·工具
这儿有一堆花9 小时前
安卓应用卡顿、性能低下的背后原因
android·安卓