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

 

GitHub 加速计划 / js / json
41.72 K
6.61 K
下载
适用于现代 C++ 的 JSON。
最近提交(Master分支:1 个月前 )
960b763e 3 个月前
8c391e04 6 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐