Added support for markers (#1105)

This adds new APIs for setting the min/max/min and max frame based on a marker comment name.
Fixes #487
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e274a98..77cd7af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
-# 3.0.0
+# 3.0.0-beta2
+### Features and Improvements
+* Added support for markers. You can now call `setMinFrame`, `setMaxFrame` and `setMinAndMaxFrame` with a marker name.
+
+# 3.0.0-beta1
 ### Features and Improvements
 * **Significant** mask and matte performance improvements by only calling saveLayer() on the intersection bounds of the content and mask/matte.
 * Removed **all** memory allocations during playback including autoboxing.
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
index 31a4bf9..070f921 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.kt
@@ -85,6 +85,7 @@
                 snapshotFrameBoundaries()
                 snapshotScaleTypes()
                 testDynamicProperties()
+                testMarkers()
                 snapshotter.finalizeReportAndUpload()
             }
         }
@@ -493,6 +494,18 @@
         }
     }
 
+    private suspend fun CoroutineScope.testMarkers() {
+        withAnimationView("Tests/Marker.json", "Marker", "startFrame") { animationView ->
+            animationView.setMinAndMaxFrame("Marker A")
+            animationView.frame = animationView.minFrame.toInt()
+        }
+
+        withAnimationView("Tests/Marker.json", "Marker", "endFrame") { animationView ->
+            animationView.setMinAndMaxFrame("Marker A")
+            animationView.frame = animationView.maxFrame.toInt()
+        }
+    }
+
     private suspend fun CoroutineScope.withAnimationView(
             animationName: String,
             snapshotName: String? = null,
diff --git a/LottieSample/src/main/assets/Tests/Marker.json b/LottieSample/src/main/assets/Tests/Marker.json
new file mode 100644
index 0000000..e264f81
--- /dev/null
+++ b/LottieSample/src/main/assets/Tests/Marker.json
@@ -0,0 +1 @@
+{"v":"5.3.4","fr":29.9700012207031,"ip":0,"op":61.0000024845809,"w":200,"h":200,"nm":"Marker","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[51.816,51.816],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[-67.092,-65.592],"e":[67.092,65.592],"to":[22.3639335632324,21.8639335632324],"ti":[-22.3639335632324,-21.8639335632324]},{"t":60.0000024438501}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61.0000024845809,"st":0,"bm":0}],"markers":[{"tm":14.0000005702317,"cm":"Marker A","dr":30.0000012219251},{"tm":33.0000013441176,"cm":"Marker B\r","dr":0}]}
\ No newline at end of file
diff --git a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
index 0594bc7..dac6429 100644
--- a/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
+++ b/LottieSample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
@@ -7,6 +7,7 @@
 import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieTask
+import com.airbnb.lottie.model.LottieCompositionCache
 import com.airbnb.lottie.samples.model.CompositionArgs
 import com.airbnb.mvrx.*
 import java.io.FileInputStream
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 937f1b7..88d088a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -462,6 +462,31 @@
   }
 
   /**
+   * Sets the minimum frame to the start time of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMinFrame(String markerName) {
+    lottieDrawable.setMinFrame(markerName);
+  }
+
+  /**
+   * Sets the maximum frame to the start time + duration of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMaxFrame(String markerName) {
+    lottieDrawable.setMaxFrame(markerName);
+  }
+
+  /**
+   * Sets the minimum and maximum frame to the start time and start time + duration
+   * of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMinAndMaxFrame(String markerName) {
+    lottieDrawable.setMinAndMaxFrame(markerName);
+  }
+
+  /**
    * @see #setMinFrame(int)
    * @see #setMaxFrame(int)
    */
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 2497a58..8ecc781 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -14,6 +14,7 @@
 
 import com.airbnb.lottie.model.Font;
 import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
 
 import org.json.JSONObject;
@@ -43,6 +44,7 @@
   private Map<String, LottieImageAsset> images;
   /** Map of font names to fonts */
   private Map<String, Font> fonts;
+  private List<Marker> markers;
   private SparseArrayCompat<FontCharacter> characters;
   private LongSparseArray<Layer> layerMap;
   private List<Layer> layers;
@@ -66,7 +68,8 @@
   public void init(Rect bounds, float startFrame, float endFrame, float frameRate,
       List<Layer> layers, LongSparseArray<Layer> layerMap, Map<String,
       List<Layer>> precomps, Map<String, LottieImageAsset> images,
-      SparseArrayCompat<FontCharacter> characters, Map<String, Font> fonts) {
+      SparseArrayCompat<FontCharacter> characters, Map<String, Font> fonts,
+      List<Marker> markers) {
     this.bounds = bounds;
     this.startFrame = startFrame;
     this.endFrame = endFrame;
@@ -77,6 +80,7 @@
     this.images = images;
     this.characters = characters;
     this.fonts = fonts;
+    this.markers = markers;
   }
 
   @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -168,6 +172,22 @@
     return fonts;
   }
 
+  public List<Marker> getMarkers() {
+    return markers;
+  }
+
+  @Nullable
+  public Marker getMarker(String markerName) {
+    int size = markers.size();
+    for (int i = 0; i < markers.size(); i++) {
+      Marker marker = markers.get(i);
+      if (markerName.equals(marker.name)) {
+        return marker;
+      }
+    }
+    return null;
+  }
+
   public boolean hasImages() {
     return !images.isEmpty();
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index c9aa349..59f69c7 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -12,18 +12,20 @@
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.util.Log;
+import android.view.View;
+
 import androidx.annotation.FloatRange;
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import android.util.Log;
-import android.view.View;
 
 import com.airbnb.lottie.manager.FontAssetManager;
 import com.airbnb.lottie.manager.ImageAssetManager;
 import com.airbnb.lottie.model.KeyPath;
+import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.CompositionLayer;
 import com.airbnb.lottie.parser.LayerParser;
 import com.airbnb.lottie.utils.LottieValueAnimator;
@@ -61,14 +63,21 @@
 
   private final Set<ColorFilterData> colorFilterData = new HashSet<>();
   private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
-  @Nullable private ImageAssetManager imageAssetManager;
-  @Nullable private String imageAssetsFolder;
-  @Nullable private ImageAssetDelegate imageAssetDelegate;
-  @Nullable private FontAssetManager fontAssetManager;
-  @Nullable FontAssetDelegate fontAssetDelegate;
-  @Nullable TextDelegate textDelegate;
+  @Nullable
+  private ImageAssetManager imageAssetManager;
+  @Nullable
+  private String imageAssetsFolder;
+  @Nullable
+  private ImageAssetDelegate imageAssetDelegate;
+  @Nullable
+  private FontAssetManager fontAssetManager;
+  @Nullable
+  FontAssetDelegate fontAssetDelegate;
+  @Nullable
+  TextDelegate textDelegate;
   private boolean enableMergePaths;
-  @Nullable private CompositionLayer compositionLayer;
+  @Nullable
+  private CompositionLayer compositionLayer;
   private int alpha = 255;
   private boolean performanceTrackingEnabled;
   /**
@@ -80,7 +89,8 @@
 
   @IntDef({RESTART, REVERSE})
   @Retention(RetentionPolicy.SOURCE)
-  public @interface RepeatMode {}
+  public @interface RepeatMode {
+  }
 
   /**
    * When the animation reaches the end and <code>repeatCount</code> is INFINITE
@@ -100,7 +110,8 @@
 
   public LottieDrawable() {
     animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-      @Override public void onAnimationUpdate(ValueAnimator animation) {
+      @Override
+      public void onAnimationUpdate(ValueAnimator animation) {
         if (compositionLayer != null) {
           compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
         }
@@ -128,7 +139,7 @@
 
   /**
    * Enable this to get merge path support for devices running KitKat (19) and above.
-   *
+   * <p>
    * Merge paths currently don't work if the the operand shape is entirely contained within the
    * first shape. If you need to cut out one shape from another shape, use an even-odd fill type
    * instead of using merge paths.
@@ -156,15 +167,15 @@
    * If you use image assets, you must explicitly specify the folder in assets/ in which they are
    * located because bodymovin uses the name filenames across all compositions (img_#).
    * Do NOT rename the images themselves.
-   *
+   * <p>
    * If your images are located in src/main/assets/airbnb_loader/ then call
    * `setImageAssetsFolder("airbnb_loader/");`.
-   *
-   *
+   * <p>
+   * <p>
    * If you use LottieDrawable directly, you MUST call {@link #recycleBitmaps()} when you
    * 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.
-   *
+   * <p>
    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
    * from After Effects. If your images look like they could be represented with vector shapes,
    * see if it is possible to convert them to shape layers and re-export your animation. Check
@@ -175,7 +186,8 @@
     this.imageAssetsFolder = imageAssetsFolder;
   }
 
-  @Nullable public String getImageAssetsFolder() {
+  @Nullable
+  public String getImageAssetsFolder() {
     return imageAssetsFolder;
   }
 
@@ -244,7 +256,8 @@
     invalidateSelf();
   }
 
-  @Override public void invalidateSelf() {
+  @Override
+  public void invalidateSelf() {
     if (isDirty) {
       return;
     }
@@ -255,23 +268,28 @@
     }
   }
 
-  @Override public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
+  @Override
+  public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
     this.alpha = alpha;
   }
 
-  @Override public int getAlpha() {
+  @Override
+  public int getAlpha() {
     return alpha;
   }
 
-  @Override public void setColorFilter(@Nullable ColorFilter colorFilter) {
+  @Override
+  public void setColorFilter(@Nullable ColorFilter colorFilter) {
     Log.w(L.TAG, "Use addColorFilter instead.");
   }
 
-  @Override public int getOpacity() {
+  @Override
+  public int getOpacity() {
     return PixelFormat.TRANSLUCENT;
   }
 
-  @Override public void draw(@NonNull Canvas canvas) {
+  @Override
+  public void draw(@NonNull Canvas canvas) {
     isDirty = false;
     L.beginSection("Drawable#draw");
     if (compositionLayer == null) {
@@ -323,16 +341,19 @@
 // <editor-fold desc="animator">
 
   @MainThread
-  @Override public void start() {
+  @Override
+  public void start() {
     playAnimation();
   }
 
   @MainThread
-  @Override public void stop() {
+  @Override
+  public void stop() {
     endAnimation();
   }
 
-  @Override public boolean isRunning() {
+  @Override
+  public boolean isRunning() {
     return isAnimating();
   }
 
@@ -344,7 +365,8 @@
   public void playAnimation() {
     if (compositionLayer == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           playAnimation();
         }
       });
@@ -367,7 +389,8 @@
   public void resumeAnimation() {
     if (compositionLayer == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           resumeAnimation();
         }
       });
@@ -402,16 +425,17 @@
   /**
    * Sets the minimum progress that the animation will start from when playing or looping.
    */
-   public void setMinProgress(final float minProgress) {
-     if (composition == null) {
-       lazyCompositionTasks.add(new LazyCompositionTask() {
-         @Override public void run(LottieComposition composition) {
-           setMinProgress(minProgress);
-         }
-       });
-       return;
-     }
-   setMinFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress));
+  public void setMinProgress(final float minProgress) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override
+        public void run(LottieComposition composition) {
+          setMinProgress(minProgress);
+        }
+      });
+      return;
+    }
+    setMinFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress));
   }
 
   /**
@@ -443,7 +467,8 @@
   public void setMaxProgress(@FloatRange(from = 0f, to = 1f) final float maxProgress) {
     if (composition == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           setMaxProgress(maxProgress);
         }
       });
@@ -453,6 +478,71 @@
   }
 
   /**
+   * Sets the minimum frame to the start time of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMinFrame(final String markerName) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override
+        public void run(LottieComposition composition) {
+          setMinFrame(markerName);
+        }
+      });
+      return;
+    }
+    Marker marker = composition.getMarker(markerName);
+    if (marker == null) {
+      throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
+    }
+    setMinFrame((int) marker.startFrame);
+  }
+
+  /**
+   * Sets the maximum frame to the start time + duration of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMaxFrame(final String markerName) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override
+        public void run(LottieComposition composition) {
+          setMaxFrame(markerName);
+        }
+      });
+      return;
+    }
+    Marker marker = composition.getMarker(markerName);
+    if (marker == null) {
+      throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
+    }
+    setMaxFrame((int) (marker.startFrame + marker.durationFrames));
+  }
+
+  /**
+   * Sets the minimum and maximum frame to the start time and start time + duration
+   * of the specified marker.
+   * @throws IllegalArgumentException if the marker is not found.
+   */
+  public void setMinAndMaxFrame(final String markerName) {
+    if (composition == null) {
+      lazyCompositionTasks.add(new LazyCompositionTask() {
+        @Override
+        public void run(LottieComposition composition) {
+          setMinAndMaxFrame(markerName);
+        }
+      });
+      return;
+    }
+    Marker marker = composition.getMarker(markerName);
+    if (marker == null) {
+      throw new IllegalArgumentException("Cannot find marker with name " + markerName + ".");
+    }
+    int startFrame = (int) marker.startFrame;
+    setMinAndMaxFrame(startFrame, startFrame + (int) marker.durationFrames);
+  }
+
+  /**
    * @see #setMinFrame(int)
    * @see #setMaxFrame(int)
    */
@@ -478,7 +568,8 @@
       @FloatRange(from = 0f, to = 1f) final float maxProgress) {
     if (composition == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           setMinAndMaxProgress(minProgress, maxProgress);
         }
       });
@@ -486,11 +577,12 @@
     }
 
     setMinAndMaxFrame((int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), minProgress),
-                      (int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
+        (int) MiscUtils.lerp(composition.getStartFrame(), composition.getEndFrame(), maxProgress));
   }
 
   /**
    * Reverses the current animation speed. This does NOT play the animation.
+   *
    * @see #setSpeed(float)
    * @see #playAnimation()
    * @see #resumeAnimation()
@@ -545,7 +637,8 @@
   public void setFrame(final int frame) {
     if (composition == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           setFrame(frame);
         }
       });
@@ -565,7 +658,8 @@
   public void setProgress(@FloatRange(from = 0f, to = 1f) final float progress) {
     if (composition == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           setProgress(progress);
         }
       });
@@ -575,7 +669,6 @@
   }
 
   /**
-   *
    * @see #setRepeatCount(int)
    */
   @Deprecated
@@ -591,7 +684,7 @@
    * @param mode {@link #RESTART} or {@link #REVERSE}
    */
   public void setRepeatMode(@RepeatMode int mode) {
-      animator.setRepeatMode(mode);
+    animator.setRepeatMode(mode);
   }
 
   /**
@@ -640,12 +733,12 @@
   /**
    * Set the scale on the current composition. The only cost of this function is re-rendering the
    * current frame so you may call it frequent to scale something up or down.
-   *
+   * <p>
    * The smaller the animation is, the better the performance will be. You may find that scaling an
    * animation down then rendering it in a larger ImageView and letting ImageView scale it back up
    * with a scaleType such as centerInside will yield better performance with little perceivable
    * quality loss.
-   *
+   * <p>
    * You can also use a fixed view width/height in conjunction with the normal ImageView
    * scaleTypes centerCrop and centerInside.
    */
@@ -658,7 +751,7 @@
    * Use this if you can't bundle images with your app. This may be useful if you download the
    * 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.
-   *
+   * <p>
    * Be wary if you are using many images, however. Lottie is designed to work with vector shapes
    * from After Effects. If your images look like they could be represented with vector shapes,
    * see if it is possible to convert them to shape layers and re-export your animation. Check
@@ -688,7 +781,8 @@
     this.textDelegate = textDelegate;
   }
 
-  @Nullable public TextDelegate getTextDelegate() {
+  @Nullable
+  public TextDelegate getTextDelegate() {
     return textDelegate;
   }
 
@@ -728,18 +822,20 @@
     return animator.getAnimatedValueAbsolute();
   }
 
-  @Override public int getIntrinsicWidth() {
+  @Override
+  public int getIntrinsicWidth() {
     return composition == null ? -1 : (int) (composition.getBounds().width() * getScale());
   }
 
-  @Override public int getIntrinsicHeight() {
+  @Override
+  public int getIntrinsicHeight() {
     return composition == null ? -1 : (int) (composition.getBounds().height() * getScale());
   }
 
   /**
    * Takes a {@link KeyPath}, potentially with wildcards or globstars and resolve it to a list of
    * zero or more actual {@link KeyPath Keypaths} that exist in the current animation.
-   *
+   * <p>
    * If you want to set value callbacks for any of these values, it is recommend to use the
    * returned {@link KeyPath} objects because they will be internally resolved to their content
    * and won't trigger a tree walk of the animation contents when applied.
@@ -757,7 +853,7 @@
   /**
    * Add an property callback for the specified {@link KeyPath}. This {@link KeyPath} can resolve
    * to multiple contents. In that case, the callbacks's value will apply to all of them.
-   *
+   * <p>
    * Internally, this will check if the {@link KeyPath} has already been resolved with
    * {@link #resolveKeyPath(KeyPath)} and will resolve it if it hasn't.
    */
@@ -765,7 +861,8 @@
       final KeyPath keyPath, final T property, final LottieValueCallback<T> callback) {
     if (compositionLayer == null) {
       lazyCompositionTasks.add(new LazyCompositionTask() {
-        @Override public void run(LottieComposition composition) {
+        @Override
+        public void run(LottieComposition composition) {
           addValueCallback(keyPath, property, callback);
         }
       });
@@ -801,9 +898,10 @@
    * drawable.addValueCallback(yourKeyPath, LottieProperty.COLOR) { yourColor }
    */
   public <T> void addValueCallback(KeyPath keyPath, T property,
-      final SimpleLottieValueCallback<T> callback) {
+                                   final SimpleLottieValueCallback<T> callback) {
     addValueCallback(keyPath, property, new LottieValueCallback<T>() {
-      @Override public T getValue(LottieFrameInfo<T> frameInfo) {
+      @Override
+      public T getValue(LottieFrameInfo<T> frameInfo) {
         return callback.getValue(frameInfo);
       }
     });
@@ -821,7 +919,7 @@
     ImageAssetManager bm = getImageAssetManager();
     if (bm == null) {
       Log.w(L.TAG, "Cannot update bitmap. Most likely the drawable is not added to a View " +
-        "which prevents Lottie from getting a Context.");
+          "which prevents Lottie from getting a Context.");
       return null;
     }
     Bitmap ret = bm.updateBitmap(id, bitmap);
@@ -829,7 +927,8 @@
     return ret;
   }
 
-  @Nullable public Bitmap getImageAsset(String id) {
+  @Nullable
+  public Bitmap getImageAsset(String id) {
     ImageAssetManager bm = getImageAssetManager();
     if (bm != null) {
       return bm.bitmapForId(id);
@@ -855,7 +954,8 @@
     return imageAssetManager;
   }
 
-  @Nullable public Typeface getTypeface(String fontFamily, String style) {
+  @Nullable
+  public Typeface getTypeface(String fontFamily, String style) {
     FontAssetManager assetManager = getFontAssetManager();
     if (assetManager != null) {
       return assetManager.getTypeface(fontFamily, style);
@@ -876,7 +976,8 @@
     return fontAssetManager;
   }
 
-  @Nullable private Context getContext() {
+  @Nullable
+  private Context getContext() {
     Callback callback = getCallback();
     if (callback == null) {
       return null;
@@ -892,7 +993,8 @@
    * These Drawable.Callback methods proxy the calls so that this is the drawable that is
    * actually invalidated, not a child one which will not pass the view's validateDrawable check.
    */
-  @Override public void invalidateDrawable(@NonNull Drawable who) {
+  @Override
+  public void invalidateDrawable(@NonNull Drawable who) {
     Callback callback = getCallback();
     if (callback == null) {
       return;
@@ -900,7 +1002,8 @@
     callback.invalidateDrawable(this);
   }
 
-  @Override public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+  @Override
+  public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
     Callback callback = getCallback();
     if (callback == null) {
       return;
@@ -908,7 +1011,8 @@
     callback.scheduleDrawable(this, what, when);
   }
 
-  @Override public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+  @Override
+  public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
     Callback callback = getCallback();
     if (callback == null) {
       return;
@@ -929,17 +1033,20 @@
   private static class ColorFilterData {
 
     final String layerName;
-    @Nullable final String contentName;
-    @Nullable final ColorFilter colorFilter;
+    @Nullable
+    final String contentName;
+    @Nullable
+    final ColorFilter colorFilter;
 
     ColorFilterData(@Nullable String layerName, @Nullable String contentName,
-        @Nullable ColorFilter colorFilter) {
+                    @Nullable ColorFilter colorFilter) {
       this.layerName = layerName;
       this.contentName = contentName;
       this.colorFilter = colorFilter;
     }
 
-    @Override public int hashCode() {
+    @Override
+    public int hashCode() {
       int hashCode = 17;
       if (layerName != null) {
         hashCode = hashCode * 31 * layerName.hashCode();
@@ -951,7 +1058,8 @@
       return hashCode;
     }
 
-    @Override public boolean equals(Object obj) {
+    @Override
+    public boolean equals(Object obj) {
       if (this == obj) {
         return true;
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/Marker.java b/lottie/src/main/java/com/airbnb/lottie/model/Marker.java
new file mode 100644
index 0000000..bab7413
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/model/Marker.java
@@ -0,0 +1,14 @@
+package com.airbnb.lottie.model;
+
+public class Marker {
+
+  public final String name;
+  public final float startFrame;
+  public final float durationFrames;
+
+  public Marker(String name, float startFrame, float durationFrames) {
+    this.name = name;
+    this.durationFrames = durationFrames;
+    this.startFrame = startFrame;
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
index c8a387d..53004d9 100644
--- a/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
+++ b/lottie/src/main/java/com/airbnb/lottie/parser/LottieCompositionParser.java
@@ -10,6 +10,7 @@
 import com.airbnb.lottie.LottieImageAsset;
 import com.airbnb.lottie.model.Font;
 import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
 import com.airbnb.lottie.utils.Utils;
 
@@ -35,6 +36,7 @@
     Map<String, List<Layer>> precomps = new HashMap<>();
     Map<String, LottieImageAsset> images = new HashMap<>();
     Map<String, Font> fonts = new HashMap<>();
+    List<Marker> markers = new ArrayList<>();
     SparseArrayCompat<FontCharacter> characters = new SparseArrayCompat<>();
 
     LottieComposition composition = new LottieComposition();
@@ -80,6 +82,9 @@
         case "chars":
           parseChars(reader, composition, characters);
           break;
+        case "markers":
+          parseMarkers(reader, composition, markers);
+          break;
         default:
           reader.skipValue();
       }
@@ -91,7 +96,7 @@
     Rect bounds = new Rect(0, 0, scaledWidth, scaledHeight);
 
     composition.init(bounds, startFrame, endFrame, frameRate, layers, layerMap, precomps,
-        images, characters, fonts);
+        images, characters, fonts, markers);
 
     return composition;
   }
@@ -174,7 +179,6 @@
   }
 
   private static void parseFonts(JsonReader reader, Map<String, Font> fonts) throws IOException {
-
     reader.beginObject();
     while (reader.hasNext()) {
       switch (reader.nextName()) {
@@ -203,4 +207,33 @@
     }
     reader.endArray();
   }
+
+  private static void parseMarkers(
+      JsonReader reader, LottieComposition composition, List<Marker> markers) throws IOException{
+    reader.beginArray();
+    while (reader.hasNext()) {
+      String comment = null;
+      float frame = 0f;
+      float durationFrames = 0f;
+      reader.beginObject();
+      while (reader.hasNext()) {
+        switch (reader.nextName()) {
+          case "cm":
+            comment = reader.nextString();
+            break;
+          case "tm":
+            frame = (float) reader.nextDouble();
+            break;
+          case "dr":
+            durationFrames = (float) reader.nextDouble();
+            break;
+          default:
+            reader.skipValue();
+        }
+      }
+      reader.endObject();
+      markers.add(new Marker(comment, frame, durationFrames));
+    }
+    reader.endArray();
+  }
 }
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java
index 195420e..bb42983 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieDrawableTest.java
@@ -5,6 +5,7 @@
 import androidx.collection.SparseArrayCompat;
 import com.airbnb.lottie.model.Font;
 import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
 import org.junit.Test;
 
@@ -22,7 +23,7 @@
     composition.init(new Rect(), startFrame, endFrame, 1000, new ArrayList<Layer>(),
             new LongSparseArray<Layer>(0), new HashMap<String, List<Layer>>(0),
             new HashMap<String, LottieImageAsset>(0), new SparseArrayCompat<FontCharacter>(0),
-            new HashMap<String, Font>(0));
+            new HashMap<String, Font>(0), new ArrayList<Marker>());
     return composition;
   }
 
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieValueAnimatorUnitTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieValueAnimatorUnitTest.java
index 8a582ad..08b0b8c 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieValueAnimatorUnitTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieValueAnimatorUnitTest.java
@@ -7,6 +7,7 @@
 import androidx.collection.SparseArrayCompat;
 import com.airbnb.lottie.model.Font;
 import com.airbnb.lottie.model.FontCharacter;
+import com.airbnb.lottie.model.Marker;
 import com.airbnb.lottie.model.layer.Layer;
 import com.airbnb.lottie.utils.LottieValueAnimator;
 import org.junit.Before;
@@ -62,7 +63,7 @@
     composition.init(new Rect(), startFrame, endFrame, 1000, new ArrayList<Layer>(),
             new LongSparseArray<Layer>(0), new HashMap<String, List<Layer>>(0),
             new HashMap<String, LottieImageAsset>(0), new SparseArrayCompat<FontCharacter>(0),
-            new HashMap<String, Font>(0));
+            new HashMap<String, Font>(0), new ArrayList<Marker>());
     return composition;
   }