Lottie—json文件解析

Lottie主要類圖:

Lottie對外通過控件LottieAnimationView暴露接口,控制動畫。

LottieAnimationView繼承自ImageView,通過當前時間繪製canvas顯示到界面上。這裏有兩個關鍵類:LottieComposition 負責解析json描述文件,把json內容轉成Java數據對象;LottieDrawable負責繪製,把LottieComposition轉成的數據對象繪製成drawable顯示到View上。順序如下:

json文件解析

LottieComposition負責解析json文件,建立數據到java對象的映射關係。

解析json外部結

LottieComposition封裝整個動畫的信息,包括動畫大小,動畫時長,幀率,用到的圖片,字體,圖層等等。

json外部結構

{
    "v": "5.1.13",       // bodymovin 版本
    "fr": 30,            // 幀率
    "ip": 0,             // 起始關鍵幀
    "op": 20,            // 結束關鍵幀
    "w": 150,            // 視圖寬
    "h": 130,            // 視圖高
    "nm": "鵝頭收起動畫",  // 名稱
    "ddd": 0,             // 3d
    "assets": [],        // 資源集合 
    "layers": [],        // 圖層集合
    "masker": []         // 蒙層集合
}

上圖爲一個動畫json文件,上面給出了各個參數的含義。其中ip表示其實關鍵幀,一般爲0,op表示動畫的結束關鍵幀,fr表示幀率,所以動畫時間等於:(op-ip)/fr 。w和h分別表示視圖的寬和高。

由於assets、layers、masker裏面的數據可能很大,所以上面用空數組代替。其中layers是一個圖層集合,它裏面數據一般很大,裏面包含了當前動畫的所有圖層數據,assets是一個資源集合,它裏面包含了當前動畫使用的資源圖層數據。masks則表示蒙層集合,裏面包含了所有的蒙層數據。

在lottie-android中,處理以上這些數據的代碼如下所示(刪除了一些相關性不強的代碼,完整的代碼請看lottie-android源碼):

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.nextName()) {
        case "w":
          width = reader.nextInt();
          break;
        case "h":
          height = reader.nextInt();
          break;
        case "ip":
          startFrame = (float) reader.nextDouble();
          break;
        case "op":
          endFrame = (float) reader.nextDouble() - 0.01f;
          break;
        case "fr":
          frameRate = (float) reader.nextDouble();
          break;
        case "v":
          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 "layers":
          parseLayers(reader, composition, layers, layerMap);
          break;
        case "assets":
          parseAssets(reader, composition, precomps, images);
          break;
        case "fonts":
          parseFonts(reader, fonts);             //解析字體
          break;
        case "chars":
          parseChars(reader, composition, characters);  //解析字符
          break;
        case "markers":                 //解析蒙層
          parseMarkers(reader, composition, markers);
          break;
        default:
          reader.skipValue();
      }
    }
    reader.endObject();

    int scaledWidth = (int) (width * scale);
    int scaledHeight = (int) (height * scale);
    Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);

    composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
        images, characters, fonts, markers);

    return composition;
  }

圖層元素 layer

動畫是由一個一個的圖層組合起來,並在圖層上進行偏移、縮放等操作來實現動畫的。圖層的解析是lottie的主要功能模塊。 一個layer圖層的數據格式一般如下:

{
    "ddd": 0,          // 是否爲3d
    "ind": 1,          // layer的ID,唯一
    "ty": 0,           // 圖層類型
    "nm": "鵝頭收起",   // 圖層名稱
    "refId": "comp_0", // 引用的資源,圖片/預合成層
    "sr": 1,
    "ks": {},          // 變換。對應AE中的變換設置
    layer: [],         // 該圖層包含的子圖層
    shaps: [],         // 形狀圖層
    "ao": 0,
    "w": 1334,
    "h": 750,
    "ip": 0,          // 該圖層開始關鍵幀
    "op": 60,         // 該圖層結束關鍵幀
    "st": 0,          // 該圖層
    "bm": 0
}

上面是一個layer圖層的object的格式。

其中說明一下nm屬性,該屬性是在AE中對該圖層的命名,通過在SVG中修改該命名,可以設置對應的svg的class和id。如果命名爲'#svgId',生成的對應的svg元素的id則爲'svgId';如果命名爲'.svg-class',則生成的對應的svg元素的class爲'svg-class'。

ty表示類型,例如:

  • 2: image,圖片
  • 0: comp,合成圖層
  • 1: solid;
  • 3: null;
  • 4: shape,形狀圖層
  • 5: text,文字

lottie-android中對layers圖層數據相應的處理有:

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;

    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.nextName()) {
        case "nm":
          layerName = reader.nextString();
          break;
        case "ind":
          layerId = reader.nextInt();
          break;
        case "refId":
          refId = reader.nextString();
          break;
        case "ty":
          int layerTypeInt = reader.nextInt();
          if (layerTypeInt < Layer.LayerType.UNKNOWN.ordinal()) {
            layerType = Layer.LayerType.values()[layerTypeInt];
          } else {
            layerType = Layer.LayerType.UNKNOWN;
          }
          break;
        case "parent":
          parentId = reader.nextInt();
          break;
        case "sw":
          solidWidth = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case "sh":
          solidHeight = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case "sc":
          solidColor = Color.parseColor(reader.nextString());
          break;
        case "ks":           //ks變換
          transform = AnimatableTransformParser.parse(reader, composition);
          break;
        case "tt":
          matteType = Layer.MatteType.values()[reader.nextInt()];
          composition.incrementMatteOrMaskCount(1);
          break;
        case "masksProperties":
          reader.beginArray();
          while (reader.hasNext()) {
            masks.add(MaskParser.parse(reader, composition));
          }
          composition.incrementMatteOrMaskCount(masks.size());
          reader.endArray();
          break;
        case "shapes":
          reader.beginArray();
          while (reader.hasNext()) {
            ContentModel shape = ContentModelParser.parse(reader, composition);
            if (shape != null) {
              shapes.add(shape);
            }
          }
          reader.endArray();
          break;
        case "t":
          reader.beginObject();
          while (reader.hasNext()) {
            switch (reader.nextName()) {
              case "d":
                text = AnimatableValueParser.parseDocumentData(reader, composition);
                break;
              case "a":
                reader.beginArray();
                if (reader.hasNext()) {
                  textProperties = AnimatableTextPropertiesParser.parse(reader, composition);
                }
                while (reader.hasNext()) {
                  reader.skipValue();
                }
                reader.endArray();
                break;
              default:
                reader.skipValue();
            }
          }
          reader.endObject();
          break;
        case "ef":
          reader.beginArray();
          List<String> effectNames = new ArrayList<>();
          while (reader.hasNext()) {
            reader.beginObject();
            while (reader.hasNext()) {
              switch (reader.nextName()) {
                case "nm":
                  effectNames.add(reader.nextString());
                  break;
                default:
                  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 "sr":
          timeStretch = (float) reader.nextDouble();
          break;
        case "st":
          startFrame = (float) reader.nextDouble();
          break;
        case "w":
          preCompWidth = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case "h":
          preCompHeight = (int) (reader.nextInt() * Utils.dpScale());
          break;
        case "ip":
          inFrame = (float) reader.nextDouble();
          break;
        case "op":
          outFrame = (float) reader.nextDouble();
          break;
        case "tm":
          timeRemapping = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case "cl":
          cl = reader.nextString();
          break;
        case "hd":
          hidden = reader.nextBoolean();
          break;
        default:
          reader.skipValue();
      }
    }
    reader.endObject();

    // Bodymovin pre-scales the in frame and out frame by the time stretch. However, that will
    // cause the stretch to be double counted since the in out animation gets treated the same
    // as all other animations and will have stretch applied to it again.
    inFrame /= timeStretch;
    outFrame /= timeStretch;

    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.");
    }

    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);
  }
}

ks變換

ks對應AE中圖層的變換屬性,可以通過設置錨點、位置、旋轉、縮放、透明度等來控制圖層,並設置這些屬性的變換曲線,來實現動畫。下面是一個ks屬性值:

"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
    }
}

lottie-android會把ks處理成transform的屬性,用於對元素進行變換操作。transform包含了translate(平移)、scale(縮放)、rotate(旋轉)、skew(傾斜)等幾種。lottie-android中處理ks(變換)的相關代碼爲:

 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() == JsonToken.BEGIN_OBJECT;
    if (isObject) {
      reader.beginObject();
    }
    while (reader.hasNext()) {
      switch (reader.nextName()) {
        case "a":
          reader.beginObject();
          while (reader.hasNext()) {
            if (reader.nextName().equals("k")) {
              anchorPoint = AnimatablePathValueParser.parse(reader, composition);
            } else {
              reader.skipValue();
            }
          }
          reader.endObject();
          break;
        case "p":
          position =
              AnimatablePathValueParser.parseSplitPath(reader, composition);
          break;
        case "s":
          scale = AnimatableValueParser.parseScale(reader, composition);
          break;
        case "rz":
          composition.addWarning("Lottie doesn't support 3D layers.");
        case "r":
          /**
           * Sometimes split path rotation gets exported like:
           *         "rz": {
           *           "a": 1,
           *           "k": [
           *             {}
           *           ]
           *         },
           * which doesn't parse to a real keyframe.
           */
          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 "o":
          opacity = AnimatableValueParser.parseInteger(reader, composition);
          break;
        case "so":
          startOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case "eo":
          endOpacity = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case "sk":
          skew = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        case "sa":
          skewAngle = AnimatableValueParser.parseFloat(reader, composition, false);
          break;
        default:
          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;
    }
    return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity, skew, skewAngle);
  }

shape

shape參數的值,對應AE中圖層的內容中的形狀設置的內容,其主要用於繪製圖形。下面一個shape的json爲例:

"shapes": [{
  "ty": "gr", // 類型。混合圖層
  "it": [{ // 各圖層json
      "ind": 0,
      "ty": "sh", // 類型,sh表示圖形路徑
      "ix": 1,
      "ks": {
          "a": 0,
          "k": {
              "i": [ // 內切線點集合
                  [0, 0],
                  [0, 0]
              ],
              "o": [ // 外切線點集合
                  [0, 0],
                  [0, 0]
              ],
              "v": [ // 頂點座標集合
                  [182, -321.75],
                  [206.25, -321.75]
              ], 
              "c": false // 貝塞爾路徑閉合
          },
          "ix": 2
      },
      "nm": "路徑 1",
      "mn": "ADBE Vector Shape - Group",
      "hd": false
  },{
    "ty": "st", // 類型。圖形描邊
    "c": { // 線的顏色
        "a": 0,
        "k": [0, 0, 0, 1],
        "ix": 3
    },
    "o": { // 線的不透明度
        "a": 0,
        "k": 100,
        "ix": 4
    },
    "w": { // 線的寬度
        "a": 0,
        "k": 3,
        "ix": 5
    },
    "lc": 2, // 線段的頭尾樣式
    "lj": 1, // 線段的連接樣式
    "ml": 4, // 尖角限制
    "nm": "描邊 1",
    "mn": "ADBE Vector Graphic - Stroke",
    "hd": false
  }]
}]

上面是一個shape形狀的json示例,可以看出不同的shape類型,參數也不同。shape對應的是AE中的圖層的內容的設置。shape中的ty字段表示shape的類型,ty有以下幾種:

  • gr: 圖形合併
  • st: 圖形描邊
  • fl: 圖形填充
  • tr: 圖形變換
  • sh: 圖形路徑
  • el: 橢圓路徑
  • rc: 矩形路徑
  • tm: 剪裁路徑

lottie-android中處理shape的相關代碼爲:

static ContentModel parse(JsonReader reader, LottieComposition composition)
      throws IOException {
    String type = null;

    reader.beginObject();
    // Unfortunately, for an ellipse, d is before "ty" which means that it will get parsed
    // before we are in the ellipse parser.
    // "d" is 2 for normal and 3 for reversed.
    int d = 2;
    typeLoop:
    while (reader.hasNext()) {
      switch (reader.nextName()) {
        case "ty":
          type = reader.nextString();
          break typeLoop;
        case "d":
          d = reader.nextInt();
          break;
        default:
          reader.skipValue();
      }
    }

    if (type == null) {
      return null;
    }

    ContentModel model = null;
    switch (type) {
      case "gr":
        model = ShapeGroupParser.parse(reader, composition);
        break;
      case "st":
        model = ShapeStrokeParser.parse(reader, composition);
        break;
      case "gs":
        model = GradientStrokeParser.parse(reader, composition);
        break;
      case "fl":
        model = ShapeFillParser.parse(reader, composition);
        break;
      case "gf":
        model = GradientFillParser.parse(reader, composition);
        break;
      case "tr":
        model = AnimatableTransformParser.parse(reader, composition);
        break;
      case "sh":
        model = ShapePathParser.parse(reader, composition);
        break;
      case "el":
        model = CircleShapeParser.parse(reader, composition, d);
        break;
      case "rc":
        model = RectangleShapeParser.parse(reader, composition);
        break;
      case "tm":
        model = ShapeTrimPathParser.parse(reader, composition);
        break;
      case "sr":
        model = PolystarShapeParser.parse(reader, composition);
        break;
      case "mm":
        model = MergePathsParser.parse(reader);
        composition.addWarning("Animation contains merge paths. Merge paths are only " +
            "supported on KitKat+ and must be manually enabled by calling " +
            "enableMergePathsForKitKatAndAbove().");
        break;
      case "rp":
        model = RepeaterParser.parse(reader, composition);
        break;
      default:
        Log.w(L.TAG, "Unknown shape type " + type);
    }

    while (reader.hasNext()) {
      reader.skipValue();
    }
    reader.endObject();

    return model;
  }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章