Add KeyframesWrapper to improve setProgress() performance (#1426)

To improve performance of BaseKeyframeAnimation.getCurrentKeyframe() called by setProgress() of BaseKeyframeAnimation who has only one Keyframe, SingleKeyframeWrapper was implemented with KeyframeWrapper interface.
At LottieLogo1.json, processing time of setProgress() was reduced about 70%.
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
index 8439f64..65599ab 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/keyframe/BaseKeyframeAnimation.java
@@ -1,18 +1,16 @@
 package com.airbnb.lottie.animation.keyframe;
 
-import android.util.Log;
+import androidx.annotation.FloatRange;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.airbnb.lottie.L;
 import com.airbnb.lottie.value.Keyframe;
 import com.airbnb.lottie.value.LottieValueCallback;
 
-import java.security.Key;
 import java.util.ArrayList;
 import java.util.List;
 
-import androidx.annotation.FloatRange;
-import androidx.annotation.Nullable;
-
 /**
  * @param <K> Keyframe type
  * @param <A> Animation type
@@ -26,21 +24,17 @@
   final List<AnimationListener> listeners = new ArrayList<>(1);
   private boolean isDiscrete = false;
 
-  private final List<? extends Keyframe<K>> keyframes;
+  private final KeyframesWrapper<K> keyframesWrapper;
   private float progress = 0f;
   @Nullable protected LottieValueCallback<A> valueCallback;
 
-  @Nullable private Keyframe<K> cachedKeyframe;
-
-  @Nullable private Keyframe<K> cachedGetValueKeyframe;
-  private float cachedGetValueProgress = -1f;
   @Nullable private A cachedGetValue = null;
 
   private float cachedStartDelayProgress = -1f;
   private float cachedEndProgress = -1f;
 
   BaseKeyframeAnimation(List<? extends Keyframe<K>> keyframes) {
-    this.keyframes = keyframes;
+    keyframesWrapper = wrap(keyframes);
   }
 
   public void setIsDiscrete() {
@@ -52,12 +46,9 @@
   }
 
   public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
-    if (keyframes.isEmpty()) {
+    if (keyframesWrapper.isEmpty()) {
       return;
     }
-    // Must use hashCode() since the actual object instance will be returned
-    // from getValue() below with the new values.
-    Keyframe<K> previousKeyframe = getCurrentKeyframe();
     if (progress < getStartDelayProgress()) {
       progress = getStartDelayProgress();
     } else if (progress > getEndProgress()) {
@@ -68,10 +59,7 @@
       return;
     }
     this.progress = progress;
-    // Just trigger a change but don't compute values if there is a value callback.
-    Keyframe<K> newKeyframe = getCurrentKeyframe();
-
-    if (previousKeyframe != newKeyframe || !newKeyframe.isStatic()) {
+    if (keyframesWrapper.isValueChanged(progress)) {
       notifyListeners();
     }
   }
@@ -84,22 +72,7 @@
 
   protected Keyframe<K> getCurrentKeyframe() {
     L.beginSection("BaseKeyframeAnimation#getCurrentKeyframe");
-    if (cachedKeyframe != null && cachedKeyframe.containsProgress(progress)) {
-      L.endSection("BaseKeyframeAnimation#getCurrentKeyframe");
-      return cachedKeyframe;
-    }
-
-    Keyframe<K> keyframe = keyframes.get(keyframes.size() - 1);
-    if (progress < keyframe.getStartProgress()) {
-      for (int i = keyframes.size() - 1; i >= 0; i--) {
-        keyframe = keyframes.get(i);
-        if (keyframe.containsProgress(progress)) {
-          break;
-        }
-      }
-    }
-
-    cachedKeyframe = keyframe;
+    final Keyframe<K> keyframe = keyframesWrapper.getCurrentKeyframe();
     L.endSection("BaseKeyframeAnimation#getCurrentKeyframe");
     return keyframe;
   }
@@ -138,7 +111,7 @@
   @FloatRange(from = 0f, to = 1f)
   private float getStartDelayProgress() {
       if (cachedStartDelayProgress == -1f) {
-            cachedStartDelayProgress = keyframes.isEmpty() ? 0f : keyframes.get(0).getStartProgress();
+        cachedStartDelayProgress = keyframesWrapper.getStartDelayProgress();
       }
       return cachedStartDelayProgress;
   }
@@ -146,20 +119,18 @@
   @FloatRange(from = 0f, to = 1f)
   float getEndProgress() {
       if (cachedEndProgress == -1f) {
-        cachedEndProgress = keyframes.isEmpty() ? 1f : keyframes.get(keyframes.size() - 1).getEndProgress();
+        cachedEndProgress = keyframesWrapper.getEndProgress();
       }
       return cachedEndProgress;
   }
 
   public A getValue() {
-    Keyframe<K> keyframe = getCurrentKeyframe();
     float progress = getInterpolatedCurrentKeyframeProgress();
-    if (valueCallback == null && keyframe == cachedGetValueKeyframe && cachedGetValueProgress == progress) {
+    if (valueCallback == null && keyframesWrapper.isCachedValueEnabled(progress)) {
       return cachedGetValue;
     }
 
-    cachedGetValueKeyframe = keyframe;
-    cachedGetValueProgress = progress;
+    final Keyframe<K> keyframe = getCurrentKeyframe();
     A value = getValue(keyframe, progress);
     cachedGetValue = value;
 
@@ -185,4 +156,177 @@
    * should be able to handle values outside of that range.
    */
   abstract A getValue(Keyframe<K> keyframe, float keyframeProgress);
+
+  private static <T> KeyframesWrapper<T> wrap(List<? extends Keyframe<T>> keyframes) {
+    if (keyframes.isEmpty()) {
+      return new EmptyKeyframeWrapper<>();
+    }
+    if (keyframes.size() == 1) {
+      return new SingleKeyframeWrapper<>(keyframes);
+    }
+    return new KeyframesWrapperImpl<>(keyframes);
+  }
+
+  private interface KeyframesWrapper<T> {
+    boolean isEmpty();
+
+    boolean isValueChanged(float progress);
+
+    Keyframe<T> getCurrentKeyframe();
+
+    @FloatRange(from = 0f, to = 1f)
+    float getStartDelayProgress();
+
+    @FloatRange(from = 0f, to = 1f)
+    float getEndProgress();
+
+    boolean isCachedValueEnabled(float interpolatedProgress);
+  }
+
+  private static final class EmptyKeyframeWrapper<T> implements KeyframesWrapper<T> {
+    @Override
+    public boolean isEmpty() {
+      return true;
+    }
+
+    @Override
+    public boolean isValueChanged(float progress) {
+      return false;
+    }
+
+    @Override
+    public Keyframe<T> getCurrentKeyframe() {
+      throw new IllegalStateException("not implemented");
+    }
+
+    @Override
+    public float getStartDelayProgress() {
+      return 0f;
+    }
+
+    @Override
+    public float getEndProgress() {
+      return 1f;
+    }
+
+    @Override
+    public boolean isCachedValueEnabled(float interpolatedProgress) {
+      throw new IllegalStateException("not implemented");
+    }
+  }
+
+  private static final class SingleKeyframeWrapper<T> implements KeyframesWrapper<T> {
+    @NonNull
+    private final Keyframe<T> keyframe;
+    private float cachedInterpolatedProgress = -1f;
+
+    SingleKeyframeWrapper(List<? extends Keyframe<T>> keyframes) {
+      this.keyframe = keyframes.get(0);
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+
+    @Override
+    public boolean isValueChanged(float progress) {
+      return !keyframe.isStatic();
+    }
+
+    @Override
+    public Keyframe<T> getCurrentKeyframe() {
+      return keyframe;
+    }
+
+    @Override
+    public float getStartDelayProgress() {
+      return keyframe.getStartProgress();
+    }
+
+    @Override
+    public float getEndProgress() {
+      return keyframe.getEndProgress();
+    }
+
+    @Override
+    public boolean isCachedValueEnabled(float interpolatedProgress) {
+      if (cachedInterpolatedProgress == interpolatedProgress) {
+        return true;
+      }
+      cachedInterpolatedProgress = interpolatedProgress;
+      return false;
+    }
+  }
+
+  private static final class KeyframesWrapperImpl<T> implements KeyframesWrapper<T> {
+    private final List<? extends Keyframe<T>> keyframes;
+    @NonNull
+    private Keyframe<T> currentKeyframe;
+    private Keyframe<T> cachedCurrentKeyframe = null;
+    private float cachedInterpolatedProgress = -1f;
+
+    KeyframesWrapperImpl(List<? extends Keyframe<T>> keyframes) {
+      this.keyframes = keyframes;
+      currentKeyframe = findKeyframe(0);
+    }
+
+    @Override
+    public boolean isEmpty() {
+      return false;
+    }
+
+    @Override
+    public boolean isValueChanged(float progress) {
+      if (currentKeyframe.containsProgress(progress)) {
+        return !currentKeyframe.isStatic();
+      }
+      currentKeyframe = findKeyframe(progress);
+      return true;
+    }
+
+    private Keyframe<T> findKeyframe(float progress) {
+      Keyframe<T> keyframe = keyframes.get(keyframes.size() - 1);
+      if (progress >= keyframe.getStartProgress()) {
+        return keyframe;
+      }
+      for (int i = keyframes.size() - 2; i >= 1; i--) {
+        keyframe = keyframes.get(i);
+        if (currentKeyframe == keyframe) {
+          continue;
+        }
+        if (keyframe.containsProgress(progress)) {
+          return keyframe;
+        }
+      }
+      return keyframes.get(0);
+    }
+
+    @Override
+    @NonNull
+    public Keyframe<T> getCurrentKeyframe() {
+      return currentKeyframe;
+    }
+
+    @Override
+    public float getStartDelayProgress() {
+      return keyframes.get(0).getStartProgress();
+    }
+
+    @Override
+    public float getEndProgress() {
+      return keyframes.get(keyframes.size() - 1).getEndProgress();
+    }
+
+    @Override
+    public boolean isCachedValueEnabled(float interpolatedProgress) {
+      if (cachedCurrentKeyframe == currentKeyframe
+              && cachedInterpolatedProgress == interpolatedProgress) {
+        return true;
+      }
+      cachedCurrentKeyframe = currentKeyframe;
+      cachedInterpolatedProgress = interpolatedProgress;
+      return false;
+    }
+  }
 }