Image from Bing

In the last section, we analyzed the overall process of LottieView’s playAnimation(). We also mentioned at the end that Lottie’s animation is realized through Layer by Layer, among which CompositionLayer and BaseLayer are important. Played a notice update, distribution update function. However, the previous section did not analyze exactly what Lottie does from the Json file to the animation file (Layer) and how it is parsed. So for the rest of this video, let’s look at this part of the video.

First, you can see from the use of LottieView that parsing json and setting the LottieView is done with the following code:

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

Through this code can actually see, Lottie is a assert file parsing for Composition of the object, and then give LottieView, then this LottieCompositionFactory. FromAssert () is the process of parsing the file, So, let’s start with this method:

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(a) {
        returnfromAssetSync(appContext, fileName); }}); }Copy the code

We call a cache() method, which, as the name indicates, is cache-related, and pass in a Callable.

 private static LottieTask<LottieComposition> cache(
      @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable) {
   // If the cache is not empty, determine whether there is a cache corresponding to the cacheKey in LottieCompositionCache
    final LottieComposition cachedComposition = cacheKey == null ? null : LottieCompositionCache.getInstance().get(cacheKey);
   // If the cache is not empty, a LottieTask with the result is constructed to return the cache directly.
    if(cachedComposition ! =null) {
      return new LottieTask<>(new Callable<LottieResult<LottieComposition>>() {
        @Override
        public LottieResult<LottieComposition> call(a) {
          return newLottieResult<>(cachedComposition); }}); }// If the cache does not exist, the taskCache is checked to see if it exists.
    if(cacheKey ! =null && taskCache.containsKey(cacheKey)) {
      return taskCache.get(cacheKey);
    }
		
   // If there is no task cache, a new LottieTask is generated and callbale is passed in
    LottieTask<LottieComposition> task = new LottieTask<>(callable);
   // Add a listener and cache the result when the callback is successfully loaded
    task.addListener(new LottieListener<LottieComposition>() {
      @Override
      public void onResult(LottieComposition result) {
        if(cacheKey ! =null) { LottieCompositionCache.getInstance().put(cacheKey, result); } taskCache.remove(cacheKey); }});// Failed to load the callback
    task.addFailureListener(new LottieListener<Throwable>() {
      @Override
      public void onResult(Throwable result) { taskCache.remove(cacheKey); }}); taskCache.put(cacheKey, task);return task;
  }
Copy the code

You can see that Lottie caches the animation, but you can also see from the code that Lottie caches the animation file called the key, so if you update the animation file, you need to restart the App to make it work.

Second, LottieTask is similar to AsyncTask in that it contains a thread pool to handle asynchronous tasks, but the key is fromAssetSync(appContext, fileName). This code, look at the implementation:

 @WorkerThread
  public static LottieResult<LottieComposition> fromAssetSync(Context context, String fileName) {
    try {
      String cacheKey = "asset_" + fileName;
      // Check whether it is a ZIP package. If yes, decompress the package
      if (fileName.endsWith(".zip")) {
        return fromZipStreamSync(new ZipInputStream(context.getAssets().open(fileName)), cacheKey);
      }
      // If it is not a zip package, parse it as a Json string stream
      return fromJsonInputStreamSync(context.getAssets().open(fileName), cacheKey);
    } catch (IOException e) {
      return newLottieResult<>(e); }}Copy the code

This method is an asynchronous method, analytic animation files (zip or json file), then fromJsonInputStreamSync through a series of calls eventually call to 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); }}}Copy the code

As you can see, the specific resolution is by LottieCompositionMoshiParser resolution:

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
  );
Copy the code

Is LottieCompositionMoshiParser class above, the definition of some json object names, corresponding is a json file, Lottie animation these types is at the time of parsing, distinguish the current object as what resolution, take a look at the parse method, You can see how these types work:

public static LottieComposition parse(JsonReader reader) throws IOException {
    float scale = Utils.dpScale();		/ / zoom
    float startFrame = 0f;						/ / the start frame
    float endFrame = 0f;							/ / end frame
    float frameRate = 0f;							/ / frame rate
    final LongSparseArray<Layer> layerMap = new LongSparseArray<>(); / / the parser
    final List<Layer> layers = new ArrayList<>();     // Layer collection
    int width = 0;
    int height = 0;
    Map<String, List<Layer>> precomps = new HashMap<>();    
    Map<String, LottieImageAsset> images = new HashMap<>();    // If the animation contains a bitmap
    Map<String, Font> fonts = new HashMap<>();   / / font
    List<Marker> markers = new ArrayList<>();    / / mask
    SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();

    LottieComposition composition = new LottieComposition();
    reader.beginObject();
    while (reader.hasNext()) {
      // Each of the following types corresponds to the type declared above. You can see that different types are treated differently.
      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.01 f;
          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);

  // Generate composition and call back to LottieView
    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
        images, characters, fonts, markers);

    return composition;
  }
Copy the code

By using the above method, Lottie’s animation files can be parsedinto Layers/images/fonts/markers, etc., which are then assembled into composition and called back to LottieView. Here is an example of an animated JSON file, which can be parsed to make it clearer:

{" v ", "5.1.10", "fr" / / bodymovin version: 24, / / frame rate "IP" : 0, / / the start key frames "op" : 277, / / end key frames "w" : 110, / / animation width "h" : Height 110, / / animation "nm" : "synthesis of 2," "DDD" : 0, "assets" : [...]. // Layers: [...] } / / / / layer information assert the resource information, such as images {" id ":" image_0 ", "w" / / photo id: 750, / / picture width "h" : 1334, / / picture height "u" : "images /, / / image path" p ": "Img_0. PNG" / / picture name} / / layer information "the layers" : [{" DDD ": 0," ind ": 1, / / layer id" ty ": 2, // Layer type (PRE_COMP, SOLID, IMAGE, NULL, SHAPE, TEXT, UNKNOWN) "nm": "eye-right 2", "parent": 3, // parent layer ID "refId": "Image_0", / / reference resource Id "sr" : 1, "ks" : {/ / animation attribute value "s" : {/ / s: scaling of the data values "a" : 0, "k" : [100, 100, 100], "ix" : 6}...}, "IP" : 0, //inFrame this layer startFrame "op": 241, //outFrame this layer end frame "st": 0, //startFrame startFrame "bm": 0, "sw":0, //solidWidth "sh":0, //solidHeight "sc":0, //solidColor "tt":0, //transform }Copy the code

So, through this process, will eventually give LottieView animation files packaged into composition callback, call 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(); . }Copy the code

In this method, assign composition to LottieDrawable, and then call playAnimation. This will follow the same process we described in the first section. Note that requestLayout() is also called. When setComposition is called, the animation is displayed but not played.

Finally, give an overall flow chart of loading animation files:

At this point, the entire Lottie workflow and parsing process is sorted out, and Lottie libraries are good for projects that require good animations or have a lot of complex animations. Finally, to summarize a few key points in Lottie and some considerations:

The key class

  • LottieComposition

Contains Layer, LottieImageAsset, Font, FontCharacter

Use this class to transform AE data objects and map JSON to this class. This is then converted to Drawable.

  • LottieCompositionFactory

Create a Factory class for LottieComposition that can be loaded from the network, from a file, from Assert, and so on.

  • LottieCompositionMoshiParser

LottieComposition interpreter that parses the JSON data format into LottieComposition according to the agreed parsing rules.

  • LottieDrawable

In this class, the parsed Lotus Position is converted to Lotus EdRawable and is the primary animation bearer.

  • LayoutParser

LottieDrawable parses the animation’s JSON file into layers, including CompositionLayer, SolidLayer, ImageLayer, ShapeLayer, and TextLayer. In the end, we will animate these layers by rendering them.

There may be problems

  • The performance and time cost of rendering with masks or frosted glass/frosted effects is more than double that without these special effects. BaseLayer:
// If there is no mask or matte, return directly
if(! hasMatteOnThisLayer() && ! hasMasksOnThisLayer()) { matrix.preConcat(transform.getMatrix()); L.beginSection("Layer#drawLayer");
      drawLayer(canvas, matrix, alpha);
      L.endSection("Layer#drawLayer");
      recordRenderTime(L.endSection(drawTraceName));
      return; }...// Otherwise a method called saveLayerCompat is called, which is a very performance consuming method that requires allocating and drawing an offscreen buffer, doubling the cost of rendering.
L.beginSection("Layer#saveLayer");
saveLayerCompat(canvas, rect, contentPaint, true);
L.endSection("Layer#saveLayer");
Copy the code
  • If animation playback is slow, what is the reason? (Hardware acceleration may not be enabled)