private static <T> Keyframe<T> parseMultiDimensionalKeyframe(LottieComposition composition, JsonReader reader,
      float scale, ValueParser<T> valueParser) throws IOException {
    PointF cp1 = null;
    PointF cp2 = null;

    PointF xCp1 = null;
    PointF xCp2 = null;
    PointF yCp1 = null;
    PointF yCp2 = null;

    float startFrame = 0;
    T startValue = null;
    T endValue = null;
    boolean hold = false;
    Interpolator interpolator = null;
    Interpolator xInterpolator = null;
    Interpolator yInterpolator = null;

    // Only used by PathKeyframe
    PointF pathCp1 = null;
    PointF pathCp2 = null;

    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.selectName(NAMES)) {
        case 0: // t
          startFrame = (float) reader.nextDouble();
          break;
        case 1: // s
          startValue = valueParser.parse(reader, scale);
          break;
        case 2: // e
          endValue = valueParser.parse(reader, scale);
          break;
        case 3: // o
          if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) {
            reader.beginObject();
            float xCp1x = 0f;
            float xCp1y = 0f;
            float yCp1x = 0f;
            float yCp1y = 0f;
            while (reader.hasNext()) {
              switch (reader.selectName(INTERPOLATOR_NAMES)) {
                case 0: // x
                  if (reader.peek() == JsonReader.Token.NUMBER) {
                    xCp1x = (float) reader.nextDouble();
                    yCp1x = xCp1x;
                  } else {
                    reader.beginArray();
                    xCp1x = (float) reader.nextDouble();
                    if (reader.peek() == JsonReader.Token.NUMBER) {
                      yCp1x = (float) reader.nextDouble();
                    } else {
                      yCp1x = xCp1x;
                    }
                    reader.endArray();
                  }
                  break;
                case 1: // y
                  if (reader.peek() == JsonReader.Token.NUMBER) {
                    xCp1y = (float) reader.nextDouble();
                    yCp1y = xCp1y;
                  } else {
                    reader.beginArray();
                    xCp1y = (float) reader.nextDouble();
                    if (reader.peek() == JsonReader.Token.NUMBER) {
                      yCp1y = (float) reader.nextDouble();
                    } else {
                      yCp1y = xCp1y;
                    }
                    reader.endArray();
                  }
                  break;
                default:
                  reader.skipValue();
              }
            }
            xCp1 = new PointF(xCp1x, xCp1y);
            yCp1 = new PointF(yCp1x, yCp1y);
            reader.endObject();
          } else {
            cp1 = JsonUtils.jsonToPoint(reader, scale);
          }
          break;
        case 4: // i
          if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) {
            reader.beginObject();
            float xCp2x = 0f;
            float xCp2y = 0f;
            float yCp2x = 0f;
            float yCp2y = 0f;
            while (reader.hasNext()) {
              switch (reader.selectName(INTERPOLATOR_NAMES)) {
                case 0: // x
                  if (reader.peek() == JsonReader.Token.NUMBER) {
                    xCp2x = (float) reader.nextDouble();
                    yCp2x = xCp2x;
                  } else {
                    reader.beginArray();
                    xCp2x = (float) reader.nextDouble();
                    if (reader.peek() == JsonReader.Token.NUMBER) {
                      yCp2x = (float) reader.nextDouble();
                    } else {
                      yCp2x = xCp2x;
                    }
                    reader.endArray();
                  }
                  break;
                case 1: // y
                  if (reader.peek() == JsonReader.Token.NUMBER) {
                    xCp2y = (float) reader.nextDouble();
                    yCp2y = xCp2y;
                  } else {
                    reader.beginArray();
                    xCp2y = (float) reader.nextDouble();
                    if (reader.peek() == JsonReader.Token.NUMBER) {
                      yCp2y = (float) reader.nextDouble();
                    } else {
                      yCp2y = xCp2y;
                    }
                    reader.endArray();
                  }
                  break;
                default:
                  reader.skipValue();
              }
            }
            xCp2 = new PointF(xCp2x, xCp2y);
            yCp2 = new PointF(yCp2x, yCp2y);
            reader.endObject();
          } else {
            cp2 = JsonUtils.jsonToPoint(reader, scale);
          }
          break;
        case 5: // h
          hold = reader.nextInt() == 1;
          break;
        case 6: // to
          pathCp1 = JsonUtils.jsonToPoint(reader, scale);
          break;
        case 7: // ti
          pathCp2 = JsonUtils.jsonToPoint(reader, scale);
          break;
        default:
          reader.skipValue();
      }
    }
    reader.endObject();

    if (hold) {
      endValue = startValue;
      // TODO: create a HoldInterpolator so progress changes don't invalidate.
      interpolator = LINEAR_INTERPOLATOR;
    } else if (cp1 != null && cp2 != null) {
      interpolator = interpolatorFor(cp1, cp2);
    } else if (xCp1 != null && yCp1 != null && xCp2 != null && yCp2 != null) {
      xInterpolator = interpolatorFor(xCp1, xCp2);
      yInterpolator = interpolatorFor(yCp1, yCp2);
    } else {
      interpolator = LINEAR_INTERPOLATOR;
    }

    Keyframe<T> keyframe;
    if (xInterpolator != null && yInterpolator != null) {
      keyframe = new Keyframe<>(composition, startValue, endValue, xInterpolator, yInterpolator, startFrame, null);
    } else {
      keyframe = new Keyframe<>(composition, startValue, endValue, interpolator, startFrame, null);
    }

    keyframe.pathCp1 = pathCp1;
    keyframe.pathCp2 = pathCp2;
    return keyframe;
  }
beginObject will already be called on the keyframe so it can be differentiated with
a non animated value.
lottie/src/main/java/com/airbnb/lottie/parser/KeyframeParser.java