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

2,398 阅读7分钟

图片来自必应

在上一节,我们分析了LottieView的playAnimation()的整体流程,我们在最后也提到了,Lottie的动画就是通过一层一层的Layer实现的,其中有CompositionLayer、BaseLayer比较重要,起到了通知更新、分发更新的作用。 但是上一节没有具体分析Lottie从 Json文件到动画文件(Layer)到底做了什么,是怎么解析的。这一节的内容,我们就来看下这一部分的内容。

首先,通过LottieView的使用可以看的出来,解析json并且给LottieView设置,是通过如下代码:

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

通过这段代码实际上可以看的出来,Lottie是将一个assert文件解析为Composition这个对象,然后给LottieView,那么这个 LottieCompositionFactory.fromAssert() 就是解析文件的过程,所以,首先从这个方法看起:

public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
    // Prevent accidentally leaking an Activity.
    final Context appContext = context.getApplicationContext();
    return cache(fileName, new Callable<LottieResult<LottieComposition>>() {
      @Override
      public LottieResult<LottieComposition> call() {
        return fromAssetSync(appContext, fileName);
      }
    });
  }

可以看到这里调用了一个 cache() 方法, 从方法名就可以看出来,这是一个和缓存有关的方法,并且传入了一个Callable,那接下来看看cache的实现:

 private static LottieTask<LottieComposition> cache(
      @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable) {
   //如果cache不为空,判断LottieCompositionCache中是否有cacheKey对应的缓存
    final LottieComposition cachedComposition = cacheKey == null ? null : LottieCompositionCache.getInstance().get(cacheKey);
   //如果缓存不为空,则构造一个带有结果的LottieTask,直接返回缓存。
    if (cachedComposition != null) {
      return new LottieTask<>(new Callable<LottieResult<LottieComposition>>() {
        @Override
        public LottieResult<LottieComposition> call() {
          return new LottieResult<>(cachedComposition);
        }
      });
    }
   //如果缓存不存在,则去任务的缓存(taskCache)中查找是否有任务的缓存。
    if (cacheKey != null && taskCache.containsKey(cacheKey)) {
      return taskCache.get(cacheKey);
    }
		
   //没有任务缓存,则生成一个新的LottieTask,并将callbale传入
    LottieTask<LottieComposition> task = new LottieTask<>(callable);
   //添加监听,并且当加载成功回调之后,将结果缓存起来
    task.addListener(new LottieListener<LottieComposition>() {
      @Override
      public void onResult(LottieComposition result) {
        if (cacheKey != null) {
          LottieCompositionCache.getInstance().put(cacheKey, result);
        }
        taskCache.remove(cacheKey);
      }
    });
   //加载失败的回调
    task.addFailureListener(new LottieListener<Throwable>() {
      @Override
      public void onResult(Throwable result) {
        taskCache.remove(cacheKey);
      }
    });
    taskCache.put(cacheKey, task);
    return task;
  }

cache() 方法的调用过程注释都写的很清楚,可以看到Lottie对动画做了缓存,但是从代码也可以看出来,这个缓存是以动画文件名称做key的,所以,如果说你更新了动画文件,需要重启App才能够生效了。

其次,这里的LottieTask有点类似AsyncTask,其内部包含了一个线程池用来处理异步任务,但是具体实现实在上面代码中的callbale,关键就是 fromAssetSync(appContext, fileName); 这句代码, 看下实现:

 @WorkerThread
  public static LottieResult<LottieComposition> fromAssetSync(Context context, String fileName) {
    try {
      String cacheKey = "asset_" + fileName;
      //判断是否为zip包,是的话需要先解压
      if (fileName.endsWith(".zip")) {
        return fromZipStreamSync(new ZipInputStream(context.getAssets().open(fileName)), cacheKey);
      }
      //不是zip包则当作Json字符串流解析
      return fromJsonInputStreamSync(context.getAssets().open(fileName), cacheKey);
    } catch (IOException e) {
      return new LottieResult<>(e);
    }
  }

这个方法是一个异步方法,解析动画文件(zip或者json文件), 然后 fromJsonInputStreamSync经过一系列调用最终会调用到 fromJsonReaderSyncInternal:

private static LottieResult<LottieComposition> fromJsonReaderSyncInternal(
      com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey, boolean close) {
    try {
      LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
      LottieCompositionCache.getInstance().put(cacheKey, composition);
      return new LottieResult<>(composition);
    } catch (Exception e) {
      return new LottieResult<>(e);
    } finally {
      if (close) {
        closeQuietly(reader);
      }
    }
  }

可以看到,具体的解析是由LottieCompositionMoshiParser解析的:

private static final JsonReader.Options NAMES = JsonReader.Options.of(
      "w", // 0
      "h", // 1
      "ip", // 2
      "op", // 3
      "fr", // 4
      "v", // 5
      "layers", // 6
      "assets", // 7
      "fonts", // 8
      "chars", // 9
      "markers" // 10
  );

上面是 LottieCompositionMoshiParser 类中,对于一些json对象名称的定义,对应的是Lottie动画的Json文件,这些类型就是在解析的时候,区分将当前对象作为什么解析,来看一下parse方法,就能够明白这些类型的作用:

public static LottieComposition parse(JsonReader reader) throws IOException {
    float scale = Utils.dpScale();		// 缩放
    float startFrame = 0f;						// 起始帧
    float endFrame = 0f;							// 结束帧
    float frameRate = 0f;							//帧率
    final LongSparseArray<Layer> layerMap = new LongSparseArray<>(); //解析器
    final List<Layer> layers = new ArrayList<>();     //图层集合
    int width = 0;
    int height = 0;
    Map<String, List<Layer>> precomps = new HashMap<>();    
    Map<String, LottieImageAsset> images = new HashMap<>();    //若动画包含bitmap,则会用到
    Map<String, Font> fonts = new HashMap<>();   //字体
    List<Marker> markers = new ArrayList<>();    //遮罩
    SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();

    LottieComposition composition = new LottieComposition();
    reader.beginObject();
    while (reader.hasNext()) {
      // 以下每种类型,都对应上面声明的类型,可以看到不同的类型,都做了不同的处理。
      switch (reader.selectName(NAMES)) {
        case 0:
          width = reader.nextInt(); 
          break;
        case 1:
          height = reader.nextInt();
          break;
        case 2:
          startFrame = (float) reader.nextDouble();
          break;
        case 3:
          endFrame = (float) reader.nextDouble() - 0.01f;
          break;
        case 4:
          frameRate = (float) reader.nextDouble();
          break;
        case 5:
          String version = reader.nextString();
          String[] versions = version.split("\\.");
          int majorVersion = Integer.parseInt(versions[0]);
          int minorVersion = Integer.parseInt(versions[1]);
          int patchVersion = Integer.parseInt(versions[2]);
          if (!Utils.isAtLeastVersion(majorVersion, minorVersion, patchVersion,
              4, 4, 0)) {
            composition.addWarning("Lottie only supports bodymovin >= 4.4.0");
          }
          break;
        case 6:
          parseLayers(reader, composition, layers, layerMap);
          break;
        case 7:
          parseAssets(reader, composition, precomps, images);
          break;
        case 8:
          parseFonts(reader, fonts);
          break;
        case 9:
          parseChars(reader, composition, characters);
          break;
        case 10:
          parseMarkers(reader, composition, markers);
          break;
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    int scaledWidth = (int) (width * scale);
    int scaledHeight = (int) (height * scale);
    Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);

  //生成composition,回调给LottieView
    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
        images, characters, fonts, markers);

    return composition;
  }

通过上面的方法,可以将Lottie的动画文件解析为相应的 Layers / images / fonts / markers 等等,然后会全部组装成composition,回调给LottieView。下面是一个示例的动画json文件,可以对应这个文件再看一下解析过程,会更加清晰:

{
  "v": "5.1.10",  //bodymovin的版本
  "fr": 24,     //帧率
  "ip": 0,      //起始关键帧
  "op": 277,    //结束关键帧
  "w": 110,     //动画宽度
  "h": 110,     //动画高度
  "nm": "合成 2",
  "ddd": 0,
   "assets": [...]   //资源信息
   "layers": [...]   //图层信息
}

//assert中资源信息,如图片
{
  "id": "image_0",   //图片id
  "w": 750,       //图片宽度
  "h": 1334,      //图片高度
  "u": "images/",      //图片路径
  "p": "img_0.png"     //图片名称
}

//图层信息
"layers": [
  {
    "ddd": 0,
    "ind": 1,     //图层 id
    "ty": 2,      //图层类型 (包括PRE_COMP、 SOLID、IMAGE、NULL、SHAPE、TEXT、UNKNOWN)
    "nm": "eye-right 2",
    "parent": 3,	//父图层id
    "refId": "image_0",  //引用资源Id
    "sr": 1,
    "ks": {       //动画属性值  
        "s": {   //s:缩放的数据值
        "a": 0,
        "k": [
          100,
          100,
          100
        ],
        "ix": 6
      }...},  
    "ip": 0,   //inFrame 该图层起始关键帧
    "op": 241,   //outFrame 该图层结束关键帧
    "st": 0,    //startFrame 开始关键帧
    "bm": 0,
    "sw":0,     //solidWidth
    "sh":0,			//solidHeight
    "sc":0,			//solidColor
    "tt":0,			//transform
    
  }

所以,经过上述过程,最终会将动画文件包装成composition回调给LottieView,调用 LottieAnimationView.setComposition(composition) :

 public void setComposition(@NonNull LottieComposition composition) {
    if (L.DBG) {
      Log.v(TAG, "Set Composition \n" + composition);
    }
    lottieDrawable.setCallback(this);

    this.composition = composition;
    boolean isNewComposition = lottieDrawable.setComposition(composition);
    enableOrDisableHardwareLayer();
    if (getDrawable() == lottieDrawable && !isNewComposition) {
      return;
    }

    setImageDrawable(null);
    setImageDrawable(lottieDrawable);

    onVisibilityChanged(this, getVisibility());

    requestLayout();

   ...
  }

在这个方法中,就是将composition设置给了LottieDrawable,之后再调用playAnimation的话,就会走我们在第一节中说过的流程了,需要注意的是,这里还调用了requestLayout() ,也就是说,当调用了 setComposition 之后,动画就会显示出来,但是不会播放。

最后给一张加载动画文件整体流程图:

到这里,整个Lottie的工作流程以及解析过程就整理完成了,如果在项目中,对动画效果要求较好,或者有很多复杂动画的话,使用Lottie库还是很不错的。最后再总结一下Lottie中的几个关键点以及一些注意的事项:

关键类

  • LottieComposition

包含LayerLottieImageAssetFontFontCharacter

使用该类来转换AE的数据对象,将json映射到该类。方便之后转换为Drawable。

  • LottieCompositionFactory

创建LottieComposition的工厂类,可以从网络加载,从文件加载,从assert 加载等等。

  • LottieCompositionMoshiParser

LottieComposition解释器, 根据约定的解析规则,将json 数据格式解析为LottieComposition。

  • LottieDrawable

在该类中,将解析后的LottieComposition转换为LottieDrawable,并且是主要的动画承载者。

  • LayoutParser

LottieDrawable会将动画的json文件解析为一个一个的layer,包括CompositionLayer、SolidLayer、ImageLayer、ShapeLayer、TextLayer。最后会通过渲染这些图层、达到动画的效果。

可能存在问题

  • 在有遮罩或者毛玻璃/磨砂的效果的时候,渲染的性能与时间消耗是没有这些特殊效果的一倍以上。可以参考BaseLayer源码中:
//没有mask与matte的情况下,直接返回
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
      matrix.preConcat(transform.getMatrix());
      L.beginSection("Layer#drawLayer");
      drawLayer(canvas, matrix, alpha);
      L.endSection("Layer#drawLayer");
      recordRenderTime(L.endSection(drawTraceName));
      return;
    }

...
//否则会调用一个saveLayerCompat的方法,这是一个十分消耗性能的方法,需要分配和绘制一个offscreen的缓冲区,渲染的成本增加了一倍。
L.beginSection("Layer#saveLayer");
saveLayerCompat(canvas, rect, contentPaint, true);
L.endSection("Layer#saveLayer");
  • 如果动画的播放会比较卡,原因是什么?(原因可能是没有开启硬件加速)