Audio and video development journey (62) - json analysis of Lottie source code analysis

Posted by Sealr0x on Sun, 09 Jan 2022 11:52:05 +0100

catalogue

  1. What can Lottie do
  2. Lottie animation uses the call process
  3. Introduction to Json field
  4. Resolve to lottieposition
  5. data
  6. summary

1, What can Lottie do

In terms of realizing animation, the development cost of the native way is relatively high. airbnb's open-source lottie is supported by Android, iOS, RN and other versions. After designing the animation through AE, the designer exports json and material files through AE plug-in Bodymovin.

Yes https://lottiefiles.com/popular Look at some popular Lottie dynamics

The client can achieve the corresponding animation effect by loading, parsing, rendering and playing. This idea and design is very worthy of learning and reference. We will study and analyze the next two articles. Welcome to discuss and exchange.

2, Lottie animation uses the call process

Lottie's animation call process

1. establish LottieAnimationView
2. establish LottieDrawable
3. analysis json File as LottieComposition object
4. lottieDrawable.setComposition(lottieComposition)
   lottieAnimationView.setImageDrawable(lottieDrawable)
5. Play animation playAnimation()
stay onValueChanged Each created Drawable Redrawing will be carried out as required to achieve the effect of animation.

It is very simple for developers to use. You can refer to the use of sample in lottie source code.

3, Introduction to Json field

We use the Android wave in the demo provided by Lottie Android JSON and anima JSON as an example to learn object

Let's look at the fields in assets Object - > assets: resources

You can see that there are layers in object s and possibly in assets. What information does the layer field over there contain. Object - > layers: layers

Object - > layers - > TY: layer type

Object - > layers - > KS: layer transformation

The fields in "o, r, p, a and s" are the same. They are all a, k, X and ix. A and k are described below. x. I don't know what it's for. object->layers->ks-> o ,r, p, a, s

The fields of Lottie json are basically explained. Let's take a look at the process of parsing into Lottie position in combination with the source code of Lottie Android

4, Resolve to lottieposition

json parser, parsed as a lottieposition object

4.1 lottieposition parsing process

Lottieposition object

json parser, parsed as a lottieposition object

//The parser of lottie json is parsed into lottieposition object
public class 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
  );

  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<>();
    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://Focus on the implementation of parseLayers
          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, markers);
          break;
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    
    // w. h automatically adapts according to the pixel density
    int scaledWidth = (int) (width * scale);
    int scaledHeight = (int) (height * scale);
    Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);

    //After the json content is parsed, the composition init is called to assign the LottieComposition field to complete the work of json to LottieComposition.
    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
        images, characters, fonts, markers);

    return composition;
  }

  private static void parseLayers(JsonReader reader, LottieComposition composition,
      List<Layer> layers, LongSparseArray<Layer> layerMap) throws IOException {
    int imageCount = 0;
    reader.beginArray();
    //Traverse and parse each layer,
    while (reader.hasNext()) {
      //Layer resolution
      Layer layer = LayerParser.parse(reader, composition);
      if (layer.getLayerType() == Layer.LayerType.IMAGE) {
        imageCount++;
      }
      //Adding layers and map s to improve efficiency
      layers.add(layer);
      layerMap.put(layer.getId(), layer);

    }
    reader.endArray();
  }

4.2 analysis process of layers layer

Resolved as a Layer object

Parse layers in json into Layer objects through LayerParser

//Parse layers in json into Layer objects through LayerParser
public class LayerParser {

  //Corresponding to the fields of layers in json
  private static final JsonReader.Options NAMES = JsonReader.Options.of(
      "nm", // 0
      "ind", // 1
      "refId", // 2
      "ty", // 3
      "parent", // 4
      "sw", // 5
      "sh", // 6
      "sc", // 7
      "ks", // 8
      "tt", // 9
      "masksProperties", // 10
      "shapes", // 11
      "t", // 12
      "ef", // 13
      "sr", // 14
      "st", // 15
      "w", // 16
      "h", // 17
      "ip", // 18
      "op", // 19
      "tm", // 20
      "cl", // 21
      "hd" // 22
  );


  private static final JsonReader.Options TEXT_NAMES = JsonReader.Options.of(
      "d",
      "a"
  );

  private static final JsonReader.Options EFFECTS_NAMES = JsonReader.Options.of(
      "ty",
      "nm"
  );

  public static Layer parse(JsonReader reader, LottieComposition composition) throws IOException {
    // This should always be set by After Effects. However, if somebody wants to minify
    // and optimize their json, the name isn't critical for most cases so it can be removed.
    String layerName = "UNSET";
    Layer.LayerType layerType = null;
    String refId = null;
    long layerId = 0;
    int solidWidth = 0;
    int solidHeight = 0;
    int solidColor = 0;
    int preCompWidth = 0;
    int preCompHeight = 0;
    long parentId = -1;
    float timeStretch = 1f;
    float startFrame = 0f;
    float inFrame = 0f;
    float outFrame = 0f;
    String cl = null;
    boolean hidden = false;
    BlurEffect blurEffect = null;
    DropShadowEffect dropShadowEffect = null;

    Layer.MatteType matteType = Layer.MatteType.NONE;
    AnimatableTransform transform = null;
    AnimatableTextFrame text = null;
    AnimatableTextProperties textProperties = null;
    AnimatableFloatValue timeRemapping = null;

    List<Mask> masks = new ArrayList<>();
    List<ContentModel> shapes = new ArrayList<>();

    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.selectName(NAMES)) {
        case 0:
          layerName = reader.nextString();
          break;
        case 1:
          layerId = reader.nextInt();
          break;
        case 2:
          refId = reader.nextString();
          break;
        case 3:
          int layerTypeInt = reader.nextInt();
          //Look at layer The value of layertype corresponds to ty in json
          if (layerTypeInt < Layer.LayerType.UNKNOWN.ordinal()) {
            layerType = Layer.LayerType.values()[layerTypeInt];
          } else {
            layerType = Layer.LayerType.UNKNOWN;
          }
          break;
        case 4:
          parentId = reader.nextInt();
          break;
        case 5:
          solidWidth = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case 6:
          solidHeight = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case 7:
          solidColor = Color.parseColor(reader.nextString());
          break;
        case 8:
          //Layer transformation, the focus of animation 
          transform = AnimatableTransformParser.parse(reader, composition);
          break;
        case 9:
          int matteTypeIndex = reader.nextInt();
          if (matteTypeIndex >= Layer.MatteType.values().length) {
            composition.addWarning("Unsupported matte type: " + matteTypeIndex);
            break;
          }
          matteType = Layer.MatteType.values()[matteTypeIndex];
          switch (matteType) {
            case LUMA:
              composition.addWarning("Unsupported matte type: Luma");
              break;
            case LUMA_INVERTED:
              composition.addWarning("Unsupported matte type: Luma Inverted");
              break;
          }
          composition.incrementMatteOrMaskCount(1);
          break;
        case 10:
          //Mask analysis
          reader.beginArray();
          while (reader.hasNext()) {
            masks.add(MaskParser.parse(reader, composition));
          }
          composition.incrementMatteOrMaskCount(masks.size());
          reader.endArray();
          break;
        case 11:
          reader.beginArray();
          while (reader.hasNext()) {
            ContentModel shape = ContentModelParser.parse(reader, composition);
            if (shape != null) {
              shapes.add(shape);
            }
          }
          reader.endArray();
          break;
        case 12:
          reader.beginObject();
          while (reader.hasNext()) {
            switch (reader.selectName(TEXT_NAMES)) {
              case 0:
                text = AnimatableValueParser.parseDocumentData(reader, composition);
                break;
              case 1:
                reader.beginArray();
                if (reader.hasNext()) {
                  textProperties = AnimatableTextPropertiesParser.parse(reader, composition);
                }
                while (reader.hasNext()) {
                  reader.skipValue();
                }
                reader.endArray();
                break;
              default:
                reader.skipName();
                reader.skipValue();
            }
          }
          reader.endObject();
          break;
        case 13:
          //Special effects
          reader.beginArray();
          List<String> effectNames = new ArrayList<>();
          while (reader.hasNext()) {
            reader.beginObject();
            while (reader.hasNext()) {
              switch (reader.selectName(EFFECTS_NAMES)) {
                case 0:
                  int type = reader.nextInt();
                  if (type == 29) {
                    blurEffect = BlurEffectParser.parse(reader, composition);
                  } else if (type == 25) {
                    dropShadowEffect = new DropShadowEffectParser().parse(reader, composition);
                  }
                  break;
                case 1:
                  String effectName = reader.nextString();
                  effectNames.add(effectName);
                  break;
                default:
                  reader.skipName();
                  reader.skipValue();

              }
            }
            reader.endObject();
          }
          reader.endArray();
          composition.addWarning("Lottie doesn't support layer effects. If you are using them for " +
              " fills, strokes, trim paths etc. then try adding them directly as contents " +
              " in your shape. Found: " + effectNames);
          break;
        case 14:
          timeStretch = (float) reader.nextDouble();
          break;
        case 15:
          startFrame = (float) reader.nextDouble();
          break;
        case 16:
          preCompWidth = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case 17:
          preCompHeight = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case 18:
          inFrame = (float) reader.nextDouble();
          break;
        case 19:
          outFrame = (float) reader.nextDouble();
          break;
        case 20:
          timeRemapping = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case 21:
          cl = reader.nextString();
          break;
        case 22:
          hidden = reader.nextBoolean();
          break;
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    reader.endObject();

    List<Keyframe<Float>> inOutKeyframes = new ArrayList<>();
    // Before the in frame
    if (inFrame > 0) {
      Keyframe<Float> preKeyframe = new Keyframe<>(composition, 0f, 0f, null, 0f, inFrame);
      inOutKeyframes.add(preKeyframe);
    }

    // The + 1 is because the animation should be visible on the out frame itself.
    outFrame = (outFrame > 0 ? outFrame : composition.getEndFrame());
    Keyframe<Float> visibleKeyframe =
        new Keyframe<>(composition, 1f, 1f, null, inFrame, outFrame);
    inOutKeyframes.add(visibleKeyframe);

    Keyframe<Float> outKeyframe = new Keyframe<>(
        composition, 0f, 0f, null, outFrame, Float.MAX_VALUE);
    inOutKeyframes.add(outKeyframe);

    if (layerName.endsWith(".ai") || "ai".equals(cl)) {
      composition.addWarning("Convert your Illustrator layers to shape layers.");
    }

    //Generate the corresponding Layer object according to the value parsed above. This object is for drawing
    return new Layer(shapes, composition, layerName, layerId, layerType, parentId, refId,
        masks, transform, solidWidth, solidHeight, solidColor, timeStretch, startFrame,
        preCompWidth, preCompHeight, text, textProperties, inOutKeyframes, matteType,
        timeRemapping, hidden, blurEffect, dropShadowEffect);
  }
}

4.3 analysis process of KS layer transformation

Is resolved to an object AnimatableTransform

Parse ks in json into animatable transform objects through animatable transform parser

//Parse ks in json into animatable transform objects through animatable transform parser
public class AnimatableTransformParser {

  private static final JsonReader.Options NAMES = JsonReader.Options.of(
      "a",  // 1
      "p",  // 2
      "s",  // 3
      "rz", // 4
      "r",  // 5
      "o",  // 6
      "so", // 7
      "eo", // 8
      "sk", // 9
      "sa"  // 10
  );
  private static final JsonReader.Options ANIMATABLE_NAMES = JsonReader.Options.of("k");

  public static AnimatableTransform parse(
      JsonReader reader, LottieComposition composition) throws IOException {
    AnimatablePathValue anchorPoint = null;
    AnimatableValue<PointF, PointF> position = null;
    AnimatableScaleValue scale = null;
    AnimatableFloatValue rotation = null;
    AnimatableIntegerValue opacity = null;
    AnimatableFloatValue startOpacity = null;
    AnimatableFloatValue endOpacity = null;
    AnimatableFloatValue skew = null;
    AnimatableFloatValue skewAngle = null;

    boolean isObject = reader.peek() == JsonReader.Token.BEGIN_OBJECT;
    if (isObject) {
      reader.beginObject();
    }
    while (reader.hasNext()) {
      switch (reader.selectName(NAMES)) {
        case 0: // a
          reader.beginObject();
          while (reader.hasNext()) {
            switch (reader.selectName(ANIMATABLE_NAMES)) {
              case 0:
                anchorPoint = AnimatablePathValueParser.parse(reader, composition);
                break;
              default:
                reader.skipName();
                reader.skipValue();
            }
          }
          reader.endObject();
          break;
        case 1: // p
          position =
              AnimatablePathValueParser.parseSplitPath(reader, composition);
          break;
        case 2: // s
          scale = AnimatableValueParser.parseScale(reader, composition);
          break;
        case 3: // rz
          composition.addWarning("Lottie doesn't support 3D layers.");
        case 4: // r
          rotation = AnimatableValueParser.parseFloat(reader, composition, false);
          if (rotation.getKeyframes().isEmpty()) {
            rotation.getKeyframes().add(new Keyframe<>(composition, 0f, 0f, null, 0f, composition.getEndFrame()));
          } else if (rotation.getKeyframes().get(0).startValue == null) {
            rotation.getKeyframes().set(0, new Keyframe<>(composition, 0f, 0f, null, 0f, composition.getEndFrame()));
          }
          break;
        case 5: // o
          opacity = AnimatableValueParser.parseInteger(reader, composition);
          break;
        case 6: // so
          startOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case 7: // eo
          endOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case 8: // sk
          skew = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case 9: // sa
          skewAngle = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        default:
          reader.skipName();
          reader.skipValue();
      }
    }
    if (isObject) {
      reader.endObject();
    }

    if (isAnchorPointIdentity(anchorPoint)) {
      anchorPoint = null;
    }
    if (isPositionIdentity(position)) {
      position = null;
    }
    if (isRotationIdentity(rotation)) {
      rotation = null;
    }
    if (isScaleIdentity(scale)) {
      scale = null;
    }
    if (isSkewIdentity(skew)) {
      skew = null;
    }
    if (isSkewAngleIdentity(skewAngle)) {
      skewAngle = null;
    }
    //Generates an AnimatableTransform object based on the resolved properties
    return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity, skew, skewAngle);
  }

5, Information

  1. Use of Lottie
  2. Lottie implementation ideas and source code analysis
  3. How to understand json parameters
  4. Lottie - json file parsing
  5. What else can Lottie do?

6, Harvest

Through the study of this article

  1. Understand the flow of Lottie animation
  2. Understand the meaning of each field in lottie json
  3. Analyze lottie json parsing process

Thank you for reading In the next chapter, we will analyze and learn the implementation of Layer rendering and animation