Added support for setting the start/end frame/progress manually (#433)

Fixes #415
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AnimationFragment.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AnimationFragment.kt
index 2231edb..f1ea666 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AnimationFragment.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/AnimationFragment.kt
@@ -107,7 +107,7 @@
         ))
 
         view.animationView.addAnimatorUpdateListener {
-            animation -> seekBar.progress = (animation.animatedFraction * 100).toInt()
+            animation -> seekBar.progress = ((animation.animatedValue as Float) * 100f).toInt()
         }
 
         view.seekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
@@ -118,6 +118,11 @@
             }
         ))
 
+        view.trimView.setCallback({ startProgress, endProgress ->
+            animationView.setMinAndMaxProgress(startProgress, endProgress)
+            animationView.progress = startProgress
+        })
+
         view.scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
                 onProgressChanged = { _, progress, _ ->
                     animationView.scale = progress / SCALE_SLIDER_FACTOR
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieFontViewGroup.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieFontViewGroup.kt
index 3bda165..79d4a76 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieFontViewGroup.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/LottieFontViewGroup.kt
@@ -14,11 +14,6 @@
 import com.airbnb.lottie.LottieComposition
 import java.util.*
 
-private inline fun consume(f: () -> Unit): Boolean {
-    f()
-    return true
-}
-
 class LottieFontViewGroup @JvmOverloads constructor(
         context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TrimView.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TrimView.kt
new file mode 100644
index 0000000..a6dc2f8
--- /dev/null
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/TrimView.kt
@@ -0,0 +1,75 @@
+package com.airbnb.lottie.samples
+
+import android.content.Context
+import android.support.v4.widget.ViewDragHelper
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+
+class TrimView @JvmOverloads constructor(
+        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+    private val leftAnchor by lazy {
+        val iv = ImageView(context)
+        iv.setImageResource(R.drawable.ic_trim)
+        iv
+    }
+    private val rightAnchor by lazy {
+        val iv = ImageView(context)
+        iv.setImageResource(R.drawable.ic_trim)
+        iv
+    }
+    private lateinit var callback: (Float, Float) -> Unit
+
+    private val dragHelper = ViewDragHelper.create(this, object: ViewDragHelper.Callback() {
+        override fun tryCaptureView(child: View, pointerId: Int) = true
+
+        override fun getViewHorizontalDragRange(child: View) = width
+
+        override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
+            if (child == leftAnchor) {
+                return maxOf(minOf(left, rightAnchor.left - leftAnchor.width), 0)
+            } else {
+                return minOf(maxOf(leftAnchor.right, left), width - rightAnchor.width)
+            }
+        }
+
+        override fun onViewPositionChanged(view: View, left: Int, top: Int, dx: Int, dy: Int) {
+            val startProgress = (leftAnchor.left + leftAnchor.width / 2f) / width.toFloat()
+            val endProgress = (rightAnchor.right - leftAnchor.width / 2f) / width.toFloat()
+            callback(startProgress, endProgress)
+        }
+    })
+
+
+    init {
+        val leftLp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
+        leftLp.gravity = Gravity.START
+        leftAnchor.layoutParams = leftLp
+        addView(leftAnchor)
+        val rightLp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
+        rightLp.gravity = Gravity.END
+        rightAnchor.layoutParams = rightLp
+        addView(rightAnchor)
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
+        if (dragHelper.shouldInterceptTouchEvent(ev)) {
+            return true
+        }
+        return super.onInterceptTouchEvent(ev)
+    }
+
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        dragHelper.processTouchEvent(event)
+        return true
+    }
+
+    fun setCallback(callback: (Float, Float) -> Unit) {
+        this.callback = callback
+    }
+}
\ No newline at end of file
diff --git a/LottieSample/src/main/res/drawable/ic_trim.xml b/LottieSample/src/main/res/drawable/ic_trim.xml
new file mode 100644
index 0000000..203640d
--- /dev/null
+++ b/LottieSample/src/main/res/drawable/ic_trim.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="36dp"
+        android:height="36dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M7.41,18.59L8.83,20 12,16.83 15.17,20l1.41,-1.41L12,14l-4.59,4.59zM16.59,5.41L15.17,4 12,7.17 8.83,4 7.41,5.41 12,10l4.59,-4.59z"/>
+</vector>
diff --git a/LottieSample/src/main/res/layout/fragment_animation.xml b/LottieSample/src/main/res/layout/fragment_animation.xml
index 24cc8e5..d3c5049 100644
--- a/LottieSample/src/main/res/layout/fragment_animation.xml
+++ b/LottieSample/src/main/res/layout/fragment_animation.xml
@@ -144,8 +144,7 @@
 
         <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginBottom="6dp">
+            android:layout_height="wrap_content">
 
             <ImageButton
                 android:id="@+id/invertColors"
@@ -225,16 +224,30 @@
             <TextView
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
+                android:layout_gravity="bottom"
                 android:layout_marginRight="16dp"
                 android:layout_weight="1"
                 android:gravity="right"
                 android:text="Progress"/>
 
-            <android.support.v7.widget.AppCompatSeekBar
-                android:id="@+id/seekBar"
+            <FrameLayout
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:layout_weight="3"/>
+                android:layout_weight="3">
+                <com.airbnb.lottie.samples.TrimView
+                    android:id="@+id/trimView"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"/>
+                <android.support.v7.widget.AppCompatSeekBar
+                    android:id="@+id/seekBar"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_vertical"
+                    android:layout_marginLeft="4dp"
+                    android:layout_marginRight="4dp"/>
+            </FrameLayout>
+
 
             <Space
                 android:layout_width="0dp"
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 1d9d592..c1eafa3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -41,14 +41,13 @@
  * You can manually set the progress of the animation with {@link #setProgress(float)} or
  * {@link R.attr#lottie_progress}
  */
-public class LottieAnimationView extends AppCompatImageView {
+@SuppressWarnings({"unused", "WeakerAccess"}) public class LottieAnimationView extends AppCompatImageView {
   private static final String TAG = LottieAnimationView.class.getSimpleName();
 
   /**
    * Caching strategy for compositions that will be reused frequently.
    * Weak or Strong indicates the GC reference strength of the composition in the cache.
    */
-  @SuppressWarnings("WeakerAccess")
   public enum CacheStrategy {
     None,
     Weak,
@@ -161,7 +160,7 @@
    * @param contentName name of the specific content that the color filter is to be applied
    * @param colorFilter the color filter, null to clear the color filter
    */
-  @SuppressWarnings("unused") public void addColorFilterToContent(
+  public void addColorFilterToContent(
       String layerName, String contentName, @Nullable ColorFilter colorFilter) {
     lottieDrawable.addColorFilterToContent(layerName, contentName, colorFilter);
   }
@@ -171,7 +170,7 @@
    * @param layerName name of the layer that the color filter is to be applied
    * @param colorFilter the color filter, null to clear the color filter
    */
-  @SuppressWarnings("unused") public void addColorFilterToLayer(
+  public void addColorFilterToLayer(
       String layerName, @Nullable ColorFilter colorFilter) {
     lottieDrawable.addColorFilterToLayer(layerName, colorFilter);
   }
@@ -187,7 +186,7 @@
   /**
    * Clear all color filters on all layers and all content in the layers
    */
-  @SuppressWarnings("unused") public void clearColorFilters() {
+  public void clearColorFilters() {
     lottieDrawable.clearColorFilters();
   }
 
@@ -264,7 +263,6 @@
    * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
    * instead of using merge paths.
    */
-  @SuppressWarnings({"WeakerAccess", "Unused"})
   public void enableMergePathsForKitKatAndAbove(boolean enable) {
     lottieDrawable.enableMergePathsForKitKatAndAbove(enable);
   }
@@ -272,7 +270,6 @@
   /**
    * @see #useHardwareAcceleration(boolean)
    */
-  @SuppressWarnings({"WeakerAccess", "unused"})
   @Deprecated
   public void useExperimentalHardwareAcceleration() {
     useHardwareAcceleration(true);
@@ -282,7 +279,6 @@
   /**
    * @see #useHardwareAcceleration(boolean)
    */
-  @SuppressWarnings({"WeakerAccess", "unused"})
   @Deprecated
   public void useExperimentalHardwareAcceleration(boolean use) {
     useHardwareAcceleration(use);
@@ -291,7 +287,7 @@
   /**
    * @see #useHardwareAcceleration(boolean)
    */
-  @SuppressWarnings("unused") public void useHardwareAcceleration() {
+  public void useHardwareAcceleration() {
     useHardwareAcceleration(true);
   }
 
@@ -307,7 +303,6 @@
    *    potentially break hardware rendering with bugs in their SKIA engine. Lottie cannot do
    *    anything about that.
    */
-  @SuppressWarnings({"WeakerAccess", "unused"})
   public void useHardwareAcceleration(boolean use) {
     useHardwareLayer = use;
     enableOrDisableHardwareLayer();
@@ -319,7 +314,7 @@
    * <p>
    * Will not cache the composition once loaded.
    */
-  @SuppressWarnings("WeakerAccess") public void setAnimation(String animationName) {
+  public void setAnimation(String animationName) {
     setAnimation(animationName, defaultCacheStrategy);
   }
 
@@ -331,7 +326,6 @@
    * strong reference to the composition once it is loaded
    * and deserialized. {@link CacheStrategy#Weak} will hold a weak reference to said composition.
    */
-  @SuppressWarnings("WeakerAccess")
   public void setAnimation(final String animationName, final CacheStrategy cacheStrategy) {
     this.animationName = animationName;
     if (WEAK_REF_CACHE.containsKey(animationName)) {
@@ -414,14 +408,14 @@
   /**
    * Returns whether or not any layers in this composition has masks.
    */
-  @SuppressWarnings("unused") public boolean hasMasks() {
+  public boolean hasMasks() {
     return lottieDrawable.hasMasks();
   }
 
   /**
    * Returns whether or not any layers in this composition has a matte layer.
    */
-  @SuppressWarnings("unused") public boolean hasMatte() {
+  public boolean hasMatte() {
     return lottieDrawable.hasMatte();
   }
 
@@ -429,7 +423,6 @@
     lottieDrawable.addAnimatorUpdateListener(updateListener);
   }
 
-  @SuppressWarnings("unused")
   public void removeUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
     lottieDrawable.removeAnimatorUpdateListener(updateListener);
   }
@@ -438,7 +431,6 @@
     lottieDrawable.addAnimatorListener(listener);
   }
 
-  @SuppressWarnings("unused")
   public void removeAnimatorListener(Animator.AnimatorListener listener) {
     lottieDrawable.removeAnimatorListener(listener);
   }
@@ -461,17 +453,50 @@
     enableOrDisableHardwareLayer();
   }
 
-  @SuppressWarnings("unused") public void reverseAnimation() {
+  public void playAnimation(final int startFrame, final int endFrame) {
+    lottieDrawable.playAnimation(startFrame, endFrame);
+  }
+
+  public void playAnimation(@FloatRange(from = 0f, to = 1f) float startProgress,
+      @FloatRange(from = 0f, to = 1f) float endProgress) {
+    lottieDrawable.playAnimation(startProgress, endProgress);
+  }
+
+  public void reverseAnimation() {
     lottieDrawable.reverseAnimation();
     enableOrDisableHardwareLayer();
   }
 
-  @SuppressWarnings("unused") public void resumeReverseAnimation() {
+  public void setMinFrame(int startFrame) {
+    lottieDrawable.setMinFrame(startFrame);
+  }
+
+  public void setMinProgress(float startProgress) {
+    lottieDrawable.setMinProgress(startProgress);
+  }
+
+  public void setMaxFrame(int endFrame) {
+    lottieDrawable.setMaxFrame(endFrame);
+  }
+
+  public void setMaxProgress(float endProgress) {
+    lottieDrawable.setMaxProgress(endProgress);
+  }
+
+  public void setMinAndMaxFrame(int minFrame, int maxFrame) {
+    lottieDrawable.setMinAndMaxFrame(minFrame, maxFrame);
+  }
+
+  public void setMinAndMaxProgress(float minProgress, float maxProgress) {
+    lottieDrawable.setMinAndMaxProgress(minProgress, maxProgress);
+  }
+
+  public void resumeReverseAnimation() {
     lottieDrawable.resumeReverseAnimation();
     enableOrDisableHardwareLayer();
   }
 
-  @SuppressWarnings("unused") public void setSpeed(float speed) {
+  public void setSpeed(float speed) {
     lottieDrawable.setSpeed(speed);
   }
 
@@ -483,7 +508,7 @@
    * If your images are located in src/main/assets/airbnb_loader/ then call
    * `setImageAssetsFolder("airbnb_loader/");`.
    */
-  @SuppressWarnings("WeakerAccess") public void setImageAssetsFolder(String imageAssetsFolder) {
+  public void setImageAssetsFolder(String imageAssetsFolder) {
     lottieDrawable.setImagesAssetsFolder(imageAssetsFolder);
   }
 
@@ -499,7 +524,6 @@
    * @return the previous Bitmap or null.
    */
   @Nullable
-  @SuppressWarnings({"unused", "WeakerAccess"})
   public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
     return lottieDrawable.updateBitmap(id, bitmap);
   }
@@ -509,14 +533,14 @@
    * animations from the network or have the images saved to an SD Card. In that case, Lottie
    * will defer the loading of the bitmap to this delegate.
    */
-  @SuppressWarnings("unused") public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
+  public void setImageAssetDelegate(ImageAssetDelegate assetDelegate) {
     lottieDrawable.setImageAssetDelegate(assetDelegate);
   }
 
   /**
    * Use this to manually set fonts.
    */
-  @SuppressWarnings({"unused", "WeakerAccess"}) public void setFontAssetDelegate(
+  public void setFontAssetDelegate(
       @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) {
     lottieDrawable.setFontAssetDelegate(assetDelegate);
   }
@@ -569,7 +593,7 @@
     return lottieDrawable.getProgress();
   }
 
-  @SuppressWarnings("unused") public long getDuration() {
+  public long getDuration() {
     return composition != null ? composition.getDuration() : 0;
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 087c1ee..7879cd2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -33,7 +33,7 @@
  * handles bitmap recycling and asynchronous loading
  * of compositions.
  */
-public class LottieDrawable extends Drawable implements Drawable.Callback {
+@SuppressWarnings({"WeakerAccess", "unused"}) public class LottieDrawable extends Drawable implements Drawable.Callback {
   private static final String TAG = LottieDrawable.class.getSimpleName();
 
   private interface LazyCompositionTask {
@@ -42,7 +42,7 @@
 
   private final Matrix matrix = new Matrix();
   private LottieComposition composition;
-  private final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+  private final LottieValueAnimator animator = new LottieValueAnimator();
   private float speed = 1f;
   private float progress = 0f;
   private float scale = 1f;
@@ -61,7 +61,7 @@
   private int alpha = 255;
   private boolean performanceTrackingEnabled;
 
-  @SuppressWarnings("WeakerAccess") public LottieDrawable() {
+  public LottieDrawable() {
     animator.setRepeatCount(0);
     animator.setInterpolator(new LinearInterpolator());
     animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@@ -79,14 +79,14 @@
   /**
    * Returns whether or not any layers in this composition has masks.
    */
-  @SuppressWarnings({"unused", "WeakerAccess"}) public boolean hasMasks() {
+  public boolean hasMasks() {
     return compositionLayer != null && compositionLayer.hasMasks();
   }
 
   /**
    * Returns whether or not any layers in this composition has a matte layer.
    */
-  @SuppressWarnings({"unused", "WeakerAccess"}) public boolean hasMatte() {
+  public boolean hasMatte() {
     return compositionLayer != null && compositionLayer.hasMatte();
   }
 
@@ -101,7 +101,7 @@
    * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
    * instead of using merge paths.
    */
-  @SuppressWarnings("WeakerAccess") public void enableMergePathsForKitKatAndAbove(boolean enable) {
+  public void enableMergePathsForKitKatAndAbove(boolean enable) {
     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
       Log.w(TAG, "Merge paths are not supported pre-Kit Kat.");
       return;
@@ -125,11 +125,11 @@
    * are done. Calling {@link #recycleBitmaps()} doesn't have to be final and {@link LottieDrawable}
    * will recreate the bitmaps if needed but they will leak if you don't recycle them.
    */
-  @SuppressWarnings("WeakerAccess") public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
+  public void setImagesAssetsFolder(@Nullable String imageAssetsFolder) {
     this.imageAssetsFolder = imageAssetsFolder;
   }
 
-  @SuppressWarnings("WeakerAccess") @Nullable public String getImageAssetsFolder() {
+  @Nullable public String getImageAssetsFolder() {
     return imageAssetsFolder;
   }
 
@@ -140,7 +140,7 @@
    * will recreate the bitmaps if needed but they will leak if you don't recycle them.
    *
    */
-  @SuppressWarnings("WeakerAccess") public void recycleBitmaps() {
+  public void recycleBitmaps() {
     if (imageAssetManager != null) {
       imageAssetManager.recycleBitmaps();
     }
@@ -149,7 +149,7 @@
   /**
    * @return True if the composition is different from the previously set composition, false otherwise.
    */
-  @SuppressWarnings("WeakerAccess") public boolean setComposition(LottieComposition composition) {
+  public boolean setComposition(LottieComposition composition) {
     if (this.composition == composition) {
       return false;
     }
@@ -173,7 +173,7 @@
     return true;
   }
 
-  @SuppressWarnings("WeakerAccess") public void setPerformanceTrackingEnabled(boolean enabled) {
+  public void setPerformanceTrackingEnabled(boolean enabled) {
     performanceTrackingEnabled = enabled;
     if (composition != null) {
       composition.setPerformanceTrackingEnabled(enabled);
@@ -235,7 +235,7 @@
    * @param contentName name of the specific content that the color filter is to be applied
    * @param colorFilter the color filter, null to clear the color filter
    */
-  @SuppressWarnings("WeakerAccess") public void addColorFilterToContent(String layerName, String contentName,
+  public void addColorFilterToContent(String layerName, String contentName,
       @Nullable ColorFilter colorFilter) {
     addColorFilterInternal(layerName, contentName, colorFilter);
   }
@@ -245,7 +245,7 @@
    * @param layerName name of the layer that the color filter is to be applied
    * @param colorFilter the color filter, null to clear the color filter
    */
-  @SuppressWarnings("WeakerAccess") public void addColorFilterToLayer(String layerName, @Nullable ColorFilter colorFilter) {
+  public void addColorFilterToLayer(String layerName, @Nullable ColorFilter colorFilter) {
     addColorFilterInternal(layerName, null, colorFilter);
   }
 
@@ -260,7 +260,7 @@
   /**
    * Clear all color filters on all layers and all content in the layers
    */
-  @SuppressWarnings("WeakerAccess") public void clearColorFilters() {
+  public void clearColorFilters() {
     colorFilterData.clear();
     addColorFilterInternal(null, null, null);
   }
@@ -339,23 +339,23 @@
     systemAnimationsAreDisabled = true;
   }
 
-  @SuppressWarnings("WeakerAccess") public void loop(boolean loop) {
+  public void loop(boolean loop) {
     animator.setRepeatCount(loop ? ValueAnimator.INFINITE : 0);
   }
 
-  @SuppressWarnings("WeakerAccess") public boolean isLooping() {
+  public boolean isLooping() {
     return animator.getRepeatCount() == ValueAnimator.INFINITE;
   }
 
-  @SuppressWarnings("WeakerAccess") public boolean isAnimating() {
+  public boolean isAnimating() {
     return animator.isRunning();
   }
 
-  @SuppressWarnings("WeakerAccess") public void playAnimation() {
+  public void playAnimation() {
     playAnimation((progress > 0.0 && progress < 1.0));
   }
 
-  @SuppressWarnings("WeakerAccess") public void resumeAnimation() {
+  public void resumeAnimation() {
     playAnimation(true);
   }
 
@@ -375,11 +375,33 @@
     }
   }
 
-  @SuppressWarnings({"unused", "WeakerAccess"}) public void resumeReverseAnimation() {
+  public void playAnimation(final int startFrame, final int endFrame) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override public void run(LottieComposition composition) {
+          playAnimation(startFrame / composition.getDurationFrames(),
+              endFrame / composition.getDurationFrames());
+        }
+      });
+      return;
+    }
+    playAnimation(startFrame / composition.getDurationFrames(),
+        endFrame / composition.getDurationFrames());
+  }
+
+  public void playAnimation(@FloatRange(from = 0f, to = 1f) float startProgress,
+      @FloatRange(from = 0f, to = 1f) float endProgress) {
+    animator.updateValues(startProgress, endProgress);
+    animator.setCurrentPlayTime(0);
+    setProgress(startProgress);
+    playAnimation(false);
+  }
+
+  public void resumeReverseAnimation() {
     reverseAnimation(true);
   }
 
-  @SuppressWarnings("WeakerAccess") public void reverseAnimation() {
+  public void reverseAnimation() {
     reverseAnimation((progress > 0.0 && progress < 1.0));
   }
 
@@ -398,13 +420,51 @@
     animator.reverse();
   }
 
-  @SuppressWarnings("WeakerAccess") public void setSpeed(float speed) {
-    this.speed = speed;
-    if (speed < 0) {
-      animator.setFloatValues(1f, 0f);
-    } else {
-      animator.setFloatValues(0f, 1f);
+  public void setMinFrame(final int startFrame) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override public void run(LottieComposition composition) {
+          setMinProgress(startFrame / composition.getDurationFrames());
+        }
+      });
+      return;
     }
+    setMinProgress(startFrame / composition.getDurationFrames());
+  }
+
+   public void setMinProgress(float startProgress) {
+    animator.setStartProgress(startProgress);
+  }
+
+  public void setMaxFrame(final int endFrame) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override public void run(LottieComposition composition) {
+          setMaxProgress(endFrame / composition.getDurationFrames());
+        }
+      });
+      return;
+    }
+    setMaxProgress(endFrame / composition.getDurationFrames());
+  }
+
+  public void setMaxProgress(float endProgress) {
+    animator.setEndProgress(endProgress);
+  }
+
+  public void setMinAndMaxFrame(int minFrame, int maxFrame) {
+    setMinFrame(minFrame);
+    setMaxFrame(maxFrame);
+  }
+
+  public void setMinAndMaxProgress(float minProgress, float maxProgress) {
+    setMinProgress(minProgress);
+    setMaxProgress(maxProgress);
+  }
+
+  public void setSpeed(float speed) {
+    this.speed = speed;
+    animator.setIsReversed(speed < 0);
 
     if (composition != null) {
       animator.setDuration((long) (composition.getDuration() / Math.abs(speed)));
@@ -431,7 +491,7 @@
    * with a scaleType such as centerInside will yield better performance with little perceivable
    * quality loss.
    */
-  @SuppressWarnings("WeakerAccess") public void setScale(float scale) {
+  public void setScale(float scale) {
     this.scale = scale;
     updateBounds();
   }
@@ -441,7 +501,7 @@
    * animations from the network or have the images saved to an SD Card. In that case, Lottie
    * will defer the loading of the bitmap to this delegate.
    */
-  @SuppressWarnings({"unused", "WeakerAccess"}) public void setImageAssetDelegate(
+  public void setImageAssetDelegate(
       @SuppressWarnings("NullableProblems") ImageAssetDelegate assetDelegate) {
     this.imageAssetDelegate = assetDelegate;
     if (imageAssetManager != null) {
@@ -452,7 +512,7 @@
   /**
    * Use this to manually set fonts.
    */
-  @SuppressWarnings({"unused", "WeakerAccess"}) public void setFontAssetDelegate(
+  public void setFontAssetDelegate(
       @SuppressWarnings("NullableProblems") FontAssetDelegate assetDelegate) {
     this.fontAssetDelegate = assetDelegate;
     if (fontAssetManager != null) {
@@ -460,7 +520,6 @@
     }
   }
 
-  @SuppressWarnings("WeakerAccess")
   public void setTextDelegate(@SuppressWarnings("NullableProblems") TextDelegate textDelegate) {
     this.textDelegate = textDelegate;
   }
@@ -473,11 +532,11 @@
     return textDelegate == null && composition.getCharacters().size() > 0;
   }
 
-  @SuppressWarnings("WeakerAccess") public float getScale() {
+  public float getScale() {
     return scale;
   }
 
-  @SuppressWarnings("WeakerAccess") public LottieComposition getComposition() {
+  public LottieComposition getComposition() {
     return composition;
   }
 
@@ -490,24 +549,24 @@
         (int) (composition.getBounds().height() * scale));
   }
 
-  @SuppressWarnings("WeakerAccess") public void cancelAnimation() {
+  public void cancelAnimation() {
     lazyCompositionTasks.clear();
     animator.cancel();
   }
 
-  @SuppressWarnings("WeakerAccess") public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
+  public void addAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
     animator.addUpdateListener(updateListener);
   }
 
-  @SuppressWarnings("WeakerAccess") public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
+  public void removeAnimatorUpdateListener(ValueAnimator.AnimatorUpdateListener updateListener) {
     animator.removeUpdateListener(updateListener);
   }
 
-  @SuppressWarnings("WeakerAccess") public void addAnimatorListener(Animator.AnimatorListener listener) {
+  public void addAnimatorListener(Animator.AnimatorListener listener) {
     animator.addListener(listener);
   }
 
-  @SuppressWarnings("WeakerAccess") public void removeAnimatorListener(Animator.AnimatorListener listener) {
+  public void removeAnimatorListener(Animator.AnimatorListener listener) {
     animator.removeListener(listener);
   }
 
@@ -526,7 +585,6 @@
    * @return the previous Bitmap or null.
    */
   @Nullable
-  @SuppressWarnings({"unused", "WeakerAccess"})
   public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) {
     ImageAssetManager bm = getImageAssetManager();
     if (bm == null) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieValueAnimator.java b/lottie/src/main/java/com/airbnb/lottie/LottieValueAnimator.java
new file mode 100644
index 0000000..0190866
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieValueAnimator.java
@@ -0,0 +1,78 @@
+package com.airbnb.lottie;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+
+/**
+ * This is a slightly modified {@link ValueAnimator} that allows us to update start and end values
+ * easily optimizing for the fact that we know that it's a value animator with 2 floats.
+ */
+class LottieValueAnimator extends ValueAnimator {
+  private boolean isReversed = false;
+  private float startProgress = 0f;
+  private float endProgress = 1f;
+  private long duration;
+
+  LottieValueAnimator() {
+    setFloatValues(0f, 1f);
+
+    /*
+      This allows us to reset the values if they were temporarily reset by
+      updateValues(float, float, long, boolean)
+     */
+    addListener(new AnimatorListenerAdapter() {
+      @Override public void onAnimationEnd(Animator animation) {
+        updateValues();
+      }
+
+      @Override public void onAnimationCancel(Animator animation) {
+        updateValues();
+      }
+    });
+  }
+
+  @Override public ValueAnimator setDuration(long duration) {
+    this.duration = duration;
+    updateValues();
+    return this;
+  }
+
+  @Override public long getDuration() {
+    return duration;
+  }
+
+  void setIsReversed(boolean isReversed) {
+    this.isReversed = isReversed;
+    updateValues();
+  }
+
+  void setStartProgress(float startProgress) {
+    this.startProgress = startProgress;
+    updateValues();
+  }
+
+  void setEndProgress(float endProgress) {
+    this.endProgress = endProgress;
+    updateValues();
+  }
+
+  /**
+   * This lets you set the start and end progress for a single play of the animator. After the next
+   * time the animation ends or is cancelled, the values will be reset to those set by
+   * {@link #setStartProgress(float)} or {@link #setEndProgress(float)}.
+   */
+  void updateValues(float startProgress, float endProgress) {
+    float minValue = Math.min(startProgress, endProgress);
+    float maxValue = Math.max(startProgress, endProgress);
+    setFloatValues(
+        isReversed ? maxValue : minValue,
+        isReversed ? minValue : maxValue
+    );
+    super.setDuration((long) (duration * (maxValue - minValue)));
+  }
+
+  private void updateValues() {
+    updateValues(startProgress, endProgress);
+  }
+}