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

    float startFrame = 0;
    T startValue = null;
    T endValue = null;
    boolean hold = false;
    Interpolator interpolator = 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
          cp1 = JsonUtils.jsonToPoint(reader, 1f);
          break;
        case 4: // i
          cp2 = JsonUtils.jsonToPoint(reader, 1f);
          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 {
      interpolator = LINEAR_INTERPOLATOR;
    }

    Keyframe<T> 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