Added initial support for trim paths individually (#204)

diff --git a/LottieSample/screenshots/Tests_GradientFill.png b/LottieSample/screenshots/Tests_GradientFill.png
index d2cda77..68fdfe7 100644
--- a/LottieSample/screenshots/Tests_GradientFill.png
+++ b/LottieSample/screenshots/Tests_GradientFill.png
Binary files differ
diff --git a/LottieSample/screenshots/Tests_TrimPaths.png b/LottieSample/screenshots/Tests_TrimPaths.png
index ddf33ed..a0f6d9a 100644
--- a/LottieSample/screenshots/Tests_TrimPaths.png
+++ b/LottieSample/screenshots/Tests_TrimPaths.png
Binary files differ
diff --git a/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java b/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
index 0ff636d..1450827 100644
--- a/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/EllipseContent.java
@@ -42,7 +42,8 @@
   @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
     for (int i = 0; i < contentsBefore.size(); i++) {
       Content content = contentsBefore.get(i);
-      if (content instanceof TrimPathContent) {
+      if (content instanceof TrimPathContent &&
+          ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
         trimPath = (TrimPathContent) content;
         trimPath.addListener(this);
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java b/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
index b109242..2f107d3 100644
--- a/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/PolystarContent.java
@@ -80,7 +80,8 @@
   @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
     for (int i = 0; i < contentsBefore.size(); i++) {
       Content content = contentsBefore.get(i);
-      if (content instanceof TrimPathContent) {
+      if (content instanceof TrimPathContent &&
+          ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
         trimPath = (TrimPathContent) content;
         trimPath.addListener(this);
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java b/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
index 09e20ae..7b865cb 100644
--- a/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/RectangleContent.java
@@ -46,7 +46,8 @@
   @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
     for (int i = 0; i < contentsBefore.size(); i++) {
       Content content = contentsBefore.get(i);
-      if (content instanceof TrimPathContent) {
+      if (content instanceof TrimPathContent &&
+          ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
         trimPath = (TrimPathContent) content;
         trimPath.addListener(this);
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java b/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
index 4b3a65f..11ef25e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeContent.java
@@ -33,7 +33,9 @@
   @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
     for (int i = 0; i < contentsBefore.size(); i++) {
       Content content = contentsBefore.get(i);
-      if (content instanceof TrimPathContent) {
+      if (content instanceof TrimPathContent &&
+          ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Simultaneously) {
+        // Trim path individually will be handled by the stroke where paths are combined.
         trimPath = (TrimPathContent) content;
         trimPath.addListener(this);
       }
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
index a9da1e3..a72dea5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
@@ -3,12 +3,31 @@
 import org.json.JSONObject;
 
 class ShapeTrimPath {
+
+  enum Type {
+    Simultaneously,
+    Individually;
+
+    static Type forId(int id) {
+      switch (id) {
+        case 1:
+          return Simultaneously;
+        case 2:
+          return Individually;
+        default:
+          throw new IllegalArgumentException("Unknown trim path type " + id);
+      }
+    }
+  }
+
+  private final Type type;
   private final AnimatableFloatValue start;
   private final AnimatableFloatValue end;
   private final AnimatableFloatValue offset;
 
-  private ShapeTrimPath(AnimatableFloatValue start, AnimatableFloatValue end, AnimatableFloatValue
-      offset) {
+  private ShapeTrimPath(Type type, AnimatableFloatValue start, AnimatableFloatValue end,
+      AnimatableFloatValue offset) {
+    this.type = type;
     this.start = start;
     this.end = end;
     this.offset = offset;
@@ -20,12 +39,17 @@
 
     static ShapeTrimPath newInstance(JSONObject json, LottieComposition composition) {
       return new ShapeTrimPath(
+          Type.forId(json.optInt("m", 1)),
           AnimatableFloatValue.Factory.newInstance(json.optJSONObject("s"), composition, false),
           AnimatableFloatValue.Factory.newInstance(json.optJSONObject("e"), composition, false),
           AnimatableFloatValue.Factory.newInstance(json.optJSONObject("o"), composition, false));
     }
   }
 
+  Type getType() {
+    return type;
+  }
+
   AnimatableFloatValue getEnd() {
     return end;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java b/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
index 2eb5313..215342e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/StrokeContent.java
@@ -5,6 +5,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Path;
+import android.graphics.PathMeasure;
 import android.graphics.RectF;
 import android.support.annotation.Nullable;
 
@@ -12,11 +13,13 @@
 import java.util.List;
 
 class StrokeContent implements DrawingContent, BaseKeyframeAnimation.AnimationListener {
+  private final PathMeasure pm = new PathMeasure();
   private final Path path = new Path();
+  private final Path trimPathPath = new Path();
   private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
   private final RectF rect = new RectF();
   private final LottieDrawable lottieDrawable;
-  private final List<PathContent> paths = new ArrayList<>();
+  private final List<PathGroup> pathGroups = new ArrayList<>();
   private final float[] dashPatternValues;
 
   private final BaseKeyframeAnimation<?, Integer> colorAnimation;
@@ -75,12 +78,24 @@
   }
 
   @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
-    for (int i = 0; i < contentsAfter.size(); i++) {
+    PathGroup currentPathGroup = null;
+    for (int i = contentsAfter.size() - 1; i >= 0; i--) {
       Content content = contentsAfter.get(i);
-      if (content instanceof PathContent) {
-        paths.add((PathContent) content);
+      if (content instanceof TrimPathContent &&
+          ((TrimPathContent) content).getType() == ShapeTrimPath.Type.Individually) {
+        if (currentPathGroup != null) {
+          pathGroups.add(currentPathGroup);
+        }
+        currentPathGroup = new PathGroup((TrimPathContent) content);
+        ((TrimPathContent) content).addListener(this);
+      } else if (content instanceof PathContent) {
+        if (currentPathGroup == null) {
+          currentPathGroup = new PathGroup(null);
+        }
+        currentPathGroup.paths.add((PathContent) content);
       }
     }
+    pathGroups.add(currentPathGroup);
   }
 
   @Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
@@ -94,18 +109,92 @@
     }
     applyDashPatternIfNeeded();
 
-    path.reset();
-    for (int i = 0; i < paths.size(); i++) {
-      this.path.addPath(paths.get(i).getPath(), parentMatrix);
-    }
+    for (int i = 0; i < pathGroups.size(); i++) {
+      PathGroup pathGroup = pathGroups.get(i);
 
-    canvas.drawPath(path, paint);
+
+      if (pathGroup.trimPath != null) {
+        applyTrimPath(canvas, pathGroup, parentMatrix);
+      } else {
+        path.reset();
+        for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
+          path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
+        }
+        canvas.drawPath(path, paint);
+      }
+    }
+  }
+
+  private void applyTrimPath(Canvas canvas, PathGroup pathGroup, Matrix parentMatrix) {
+    if (pathGroup.trimPath == null) {
+      return;
+    }
+    path.reset();
+    for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
+      path.addPath(pathGroup.paths.get(j).getPath(), parentMatrix);
+    }
+    pm.setPath(path, false);
+    float totalLength = pm.getLength();
+    while (pm.nextContour()) {
+      totalLength += pm.getLength();
+    }
+    float offsetLength = totalLength * pathGroup.trimPath.getOffset().getValue() / 360f;
+    float startLength =
+        totalLength * pathGroup.trimPath.getStart().getValue() / 100f + offsetLength;
+    float endLength =
+        totalLength * pathGroup.trimPath.getEnd().getValue() / 100f + offsetLength;
+
+    float currentLength = 0;
+    for (int j = pathGroup.paths.size() - 1; j >= 0; j--) {
+      trimPathPath.set(pathGroup.paths.get(j).getPath());
+      trimPathPath.transform(parentMatrix);
+      pm.setPath(trimPathPath, false);
+      float length = pm.getLength();
+      if (endLength > totalLength && endLength - totalLength < currentLength + length &&
+          currentLength < endLength - totalLength) {
+        // Draw the segment when the end is greater than the length which wraps around to the
+        // beginning.
+        float startValue;
+        if (startLength > totalLength) {
+          startValue = (startLength - totalLength) / length;
+        } else {
+          startValue = 0;
+        }
+        float endValue = Math.min((endLength - totalLength) / length, 1);
+        Utils.applyTrimPathIfNeeded(trimPathPath, startValue, endValue, 0);
+        canvas.drawPath(trimPathPath, paint);
+      } else //noinspection StatementWithEmptyBody
+        if (currentLength + length < startLength || currentLength > endLength) {
+        // Do nothing
+      } else if (currentLength + length <= endLength && startLength < currentLength) {
+        canvas.drawPath(trimPathPath, paint);
+      } else {
+        float startValue;
+        if (startLength < currentLength) {
+          startValue = 0;
+        } else {
+          startValue = (startLength - currentLength) / length;
+        }
+        float endValue;
+        if (endLength > currentLength + length) {
+          endValue = 1f;
+        } else {
+          endValue = (endLength - currentLength) / length;
+        }
+        Utils.applyTrimPathIfNeeded(trimPathPath, startValue, endValue, 0);
+        canvas.drawPath(trimPathPath, paint);
+      }
+      currentLength += length;
+    }
   }
 
   @Override public void getBounds(RectF outBounds, Matrix parentMatrix) {
     path.reset();
-    for (int i = 0; i < paths.size(); i++) {
-      this.path.addPath(paths.get(i).getPath(), parentMatrix);
+    for (int i = 0; i < pathGroups.size(); i++) {
+      PathGroup pathGroup = pathGroups.get(i);
+      for (int j = 0; j < pathGroup.paths.size(); j++) {
+        path.addPath(pathGroup.paths.get(i).getPath(), parentMatrix);
+      }
     }
     path.computeBounds(rect, false);
 
@@ -148,4 +237,16 @@
     float offset = dashPatternOffsetAnimation == null ? 0f : dashPatternOffsetAnimation.getValue();
     paint.setPathEffect(new DashPathEffect(dashPatternValues, offset));
   }
+
+  /**
+   * Data class to help drawing trim paths individually.
+   */
+  private static final class PathGroup {
+    private final List<PathContent> paths = new ArrayList<>();
+    @Nullable private final TrimPathContent trimPath;
+
+    private PathGroup(@Nullable TrimPathContent trimPath) {
+      this.trimPath = trimPath;
+    }
+  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java b/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
index 95517b2..e3a5a65 100644
--- a/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/TrimPathContent.java
@@ -6,11 +6,13 @@
 class TrimPathContent implements Content, BaseKeyframeAnimation.AnimationListener {
 
   private final List<BaseKeyframeAnimation.AnimationListener> listeners = new ArrayList<>();
+  private final ShapeTrimPath.Type type;
   private final BaseKeyframeAnimation<?, Float> startAnimation;
   private final BaseKeyframeAnimation<?, Float> endAnimation;
   private final BaseKeyframeAnimation<?, Float> offsetAnimation;
 
   TrimPathContent(BaseLayer layer, ShapeTrimPath trimPath) {
+    type = trimPath.getType();
     startAnimation = trimPath.getStart().createAnimation();
     endAnimation = trimPath.getEnd().createAnimation();
     offsetAnimation = trimPath.getOffset().createAnimation();
@@ -38,6 +40,10 @@
     listeners.add(listener);
   }
 
+  ShapeTrimPath.Type getType() {
+    return type;
+  }
+
   public BaseKeyframeAnimation<?, Float> getStart() {
     return startAnimation;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/Utils.java b/lottie/src/main/java/com/airbnb/lottie/Utils.java
index a912aec..2ee4a6d 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Utils.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Utils.java
@@ -83,16 +83,21 @@
     if (trimPath == null) {
       return;
     }
+    applyTrimPathIfNeeded(path, trimPath.getStart().getValue() / 100f,
+        trimPath.getEnd().getValue() / 100f, trimPath.getOffset().getValue() / 360f);
+  }
 
+  static void applyTrimPathIfNeeded(
+      Path path, float startValue, float endValue, float offsetValue) {
     pathMeasure.setPath(path, false);
 
     float length = pathMeasure.getLength();
-    float start = length * trimPath.getStart().getValue() / 100f;
-    float end = length * trimPath.getEnd().getValue() / 100f;
+    float start = length * startValue;
+    float end = length * endValue;
     float newStart = Math.min(start, end);
     float newEnd = Math.max(start, end);
 
-    float offset = trimPath.getOffset().getValue() / 360f * length;
+    float offset = offsetValue * length;
     newStart += offset;
     newEnd += offset;