docs: 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.nextName()) {
          case "t":
            startFrame = (float) reader.nextDouble();
            break;
          case "s":
            startValue = valueParser.parse(reader, scale);
            break;
          case "e":
            endValue = valueParser.parse(reader, scale);
            break;
          case "o":
            cp1 = JsonUtils.jsonToPoint(reader, scale);
            break;
          case "i":
            cp2 = JsonUtils.jsonToPoint(reader, scale);
            break;
          case "h":
            hold = reader.nextInt() == 1;
            break;
          case "to":
            pathCp1 = JsonUtils.jsonToPoint(reader, scale);
            break;
          case "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) {
        cp1.x = MiscUtils.clamp(cp1.x, -scale, scale);
        cp1.y = MiscUtils.clamp(cp1.y, -MAX_CP_VALUE, MAX_CP_VALUE);
        cp2.x = MiscUtils.clamp(cp2.x, -scale, scale);
        cp2.y = MiscUtils.clamp(cp2.y, -MAX_CP_VALUE, MAX_CP_VALUE);
        int hash = Utils.hashFor(cp1.x, cp1.y, cp2.x, cp2.y);
        WeakReference<Interpolator> interpolatorRef = getInterpolator(hash);
        if (interpolatorRef != null) {
          interpolator = interpolatorRef.get();
        }
        if (interpolatorRef == null || interpolator == null) {
          interpolator = PathInterpolatorCompat.create(
              cp1.x / scale, cp1.y / scale, cp2.x / scale, cp2.y / scale);
          try {
            putInterpolator(hash, new WeakReference<>(interpolator));
          } catch (ArrayIndexOutOfBoundsException e) {
            // It is not clear why but SparseArrayCompat sometimes fails with this:
            //     https://github.com/airbnb/lottie-android/issues/452
            // Because this is not a critical operation, we can safely just ignore it.
            // I was unable to repro this to attempt a proper fix.
          }
        }
  
      } else {
        interpolator = LINEAR_INTERPOLATOR;
      }
  
      Keyframe<T> keyframe =
          new Keyframe<>(composition, startValue, endValue, interpolator, startFrame, null);
      keyframe.pathCp1 = pathCp1;
      keyframe.pathCp2 = pathCp2;
      return keyframe;
    }