Added support for repeaters (#364)

Fixes #194 
diff --git a/LottieSample/screenshots/Tests_Repeater.png b/LottieSample/screenshots/Tests_Repeater.png
new file mode 100644
index 0000000..7decfa0
--- /dev/null
+++ b/LottieSample/screenshots/Tests_Repeater.png
Binary files differ
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
index e28736b..20e31d1 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
@@ -75,6 +75,7 @@
     TestRobot.testLinearAnimation(activity, "Tests/Parenting.json");
     TestRobot.testLinearAnimation(activity, "Tests/Precomps.json");
     TestRobot.testLinearAnimation(activity, "Tests/Remap.json");
+    TestRobot.testLinearAnimation(activity, "Tests/Repeater.json");
     TestRobot.testLinearAnimation(activity, "Tests/ShapeTypes.json");
     TestRobot.testLinearAnimation(activity, "Tests/SplitDimensions.json");
     TestRobot.testLinearAnimation(activity, "Tests/Stroke.json");
diff --git a/LottieSample/src/main/assets/Tests/Repeater.json b/LottieSample/src/main/assets/Tests/Repeater.json
new file mode 100644
index 0000000..70d6e51
--- /dev/null
+++ b/LottieSample/src/main/assets/Tests/Repeater.json
@@ -0,0 +1 @@
+{"v":"4.7.0","fr":60,"ip":0,"op":120,"w":300,"h":300,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[49,48,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[65.734,65.734]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"nm":"Rectangle ","mn":"ADBE Vector Shape - Rect"},{"ty":"rp","c":{"a":0,"k":2,"ix":1},"o":{"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":[0],"e":[1]},{"t":119}],"ix":2},"m":1,"ix":2,"tr":{"ty":"tr","p":{"a":0,"k":[0,100],"ix":2},"a":{"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":[0,0],"e":[0,53],"to":[0,8.83333301544189],"ti":[0,-8.83333301544189]},{"t":119}],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"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":[0],"e":[89]},{"t":119}],"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Vertical Repeater","mn":"ADBE Vector Filter - Repeater"},{"ty":"fl","c":{"a":0,"k":[1,0,0,1]},"o":{"a":0,"k":50},"r":1,"nm":"Fill 2","mn":"ADBE Vector Graphic - Fill"},{"ty":"fl","c":{"a":0,"k":[0.132077,0,1,1]},"o":{"a":0,"k":50},"r":1,"nm":"Fill","mn":"ADBE Vector Graphic - Fill"},{"ty":"rp","c":{"a":0,"k":2,"ix":1},"o":{"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":[0],"e":[1]},{"t":119}],"ix":2},"m":1,"ix":5,"tr":{"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":[100,0],"e":[141,0],"to":[6.83333349227905,0],"ti":[-6.83333349227905,0]},{"t":119}],"ix":2},"a":{"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":[0,0],"e":[67,0],"to":[11.1666669845581,0],"ti":[-11.1666669845581,0]},{"t":119}],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"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":[0],"e":[45]},{"t":119}],"ix":4},"so":{"a":0,"k":100,"ix":5},"eo":{"a":0,"k":100,"ix":6},"nm":"Transform"},"nm":"Horizontal Repeater","mn":"ADBE Vector Filter - Repeater"}],"ip":0,"op":120,"st":0,"bm":0,"sr":1}]}
\ No newline at end of file
diff --git a/lottie/src/main/java/com/airbnb/lottie/AnimatablePathValue.java b/lottie/src/main/java/com/airbnb/lottie/AnimatablePathValue.java
index b7cf401..03c6d25 100644
--- a/lottie/src/main/java/com/airbnb/lottie/AnimatablePathValue.java
+++ b/lottie/src/main/java/com/airbnb/lottie/AnimatablePathValue.java
@@ -56,7 +56,7 @@
   }
 
   @Override
-  public BaseKeyframeAnimation<?, PointF> createAnimation() {
+  public KeyframeAnimation<PointF> createAnimation() {
     if (!hasAnimation()) {
       return new StaticKeyframeAnimation<>(initialPoint);
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
index 952e5c8..631bf01 100644
--- a/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
+++ b/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
@@ -15,13 +15,20 @@
   private final AnimatableFloatValue rotation;
   private final AnimatableIntegerValue opacity;
 
+  // Used for repeaters
+  @Nullable private final AnimatableFloatValue startOpacity;
+  @Nullable private final AnimatableFloatValue endOpacity;
+
   private AnimatableTransform(AnimatablePathValue anchorPoint, AnimatableValue<PointF> position,
-      AnimatableScaleValue scale, AnimatableFloatValue rotation, AnimatableIntegerValue opacity) {
+      AnimatableScaleValue scale, AnimatableFloatValue rotation, AnimatableIntegerValue opacity,
+      @Nullable AnimatableFloatValue startOpacity, @Nullable AnimatableFloatValue endOpacity) {
     this.anchorPoint = anchorPoint;
     this.position = position;
     this.scale = scale;
     this.rotation = rotation;
     this.opacity = opacity;
+    this.startOpacity = startOpacity;
+    this.endOpacity = endOpacity;
   }
 
   AnimatablePathValue getAnchorPoint() {
@@ -44,6 +51,14 @@
     return opacity;
   }
 
+  @Nullable public AnimatableFloatValue getStartOpacity() {
+    return startOpacity;
+  }
+
+  @Nullable public AnimatableFloatValue getEndOpacity() {
+    return endOpacity;
+  }
+
   public TransformKeyframeAnimation createAnimation() {
     return new TransformKeyframeAnimation(this);
   }
@@ -62,7 +77,10 @@
       AnimatableScaleValue scale = AnimatableScaleValue.Factory.newInstance();
       AnimatableFloatValue rotation = AnimatableFloatValue.Factory.newInstance();
       AnimatableIntegerValue opacity = AnimatableIntegerValue.Factory.newInstance();
-      return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity);
+      AnimatableFloatValue startOpacity = AnimatableFloatValue.Factory.newInstance();
+      AnimatableFloatValue endOpacity = AnimatableFloatValue.Factory.newInstance();
+      return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity, startOpacity,
+          endOpacity);
     }
 
     static AnimatableTransform newInstance(JSONObject json, LottieComposition composition) {
@@ -71,6 +89,8 @@
       AnimatableScaleValue scale;
       AnimatableFloatValue rotation = null;
       AnimatableIntegerValue opacity;
+      AnimatableFloatValue startOpacity = null;
+      AnimatableFloatValue endOpacity = null;
       JSONObject anchorJson = json.optJSONObject("a");
       if (anchorJson != null) {
         anchorPoint = new AnimatablePathValue(anchorJson.opt("k"), composition);
@@ -112,10 +132,24 @@
       if (opacityJson != null) {
         opacity = AnimatableIntegerValue.Factory.newInstance(opacityJson, composition);
       } else {
-        // Somehow some community animations don't have opacity in the transform.
+        // Repeaters have start/end opacity instead of opacity
         opacity = new AnimatableIntegerValue(Collections.<Keyframe<Integer>>emptyList(), 100);
       }
-      return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity);
+
+      JSONObject startOpacityJson = json.optJSONObject("so");
+      if (startOpacityJson != null) {
+        startOpacity =
+            AnimatableFloatValue.Factory.newInstance(startOpacityJson, composition, false);
+      }
+
+      JSONObject endOpacityJson = json.optJSONObject("eo");
+      if (endOpacityJson != null) {
+        endOpacity =
+            AnimatableFloatValue.Factory.newInstance(endOpacityJson, composition, false);
+      }
+
+      return new AnimatableTransform(
+          anchorPoint, position, scale, rotation, opacity, startOpacity, endOpacity);
     }
 
     private static void throwMissingTransform(String missingProperty) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/ContentGroup.java b/lottie/src/main/java/com/airbnb/lottie/ContentGroup.java
index afc65af..3518903 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ContentGroup.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ContentGroup.java
@@ -12,39 +12,62 @@
 
 class ContentGroup implements DrawingContent, PathContent,
     BaseKeyframeAnimation.AnimationListener {
+
+  private static List<Content> contentsFromModels(LottieDrawable drawable, BaseLayer layer,
+      List<ContentModel> contentModels) {
+    List<Content> contents = new ArrayList<>(contentModels.size());
+    for (int i = 0; i < contentModels.size(); i++) {
+      Content content = contentModels.get(i).toContent(drawable, layer);
+      if (content != null) {
+        contents.add(content);
+      }
+    }
+    return contents;
+  }
+
+  @Nullable static AnimatableTransform findTransform(List<ContentModel> contentModels) {
+    for (int i = 0; i < contentModels.size(); i++) {
+      ContentModel contentModel = contentModels.get(i);
+      if (contentModel instanceof AnimatableTransform) {
+        return (AnimatableTransform) contentModel;
+      }
+    }
+    return null;
+  }
+
   private final Matrix matrix = new Matrix();
   private final Path path = new Path();
   private final RectF rect = new RectF();
 
   private final String name;
-  private final List<Content> contents = new ArrayList<>();
+  private final List<Content> contents;
   private final LottieDrawable lottieDrawable;
   @Nullable private List<PathContent> pathContents;
   @Nullable private TransformKeyframeAnimation transformAnimation;
 
   ContentGroup(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeGroup shapeGroup) {
-    name = shapeGroup.getName();
-    this.lottieDrawable = lottieDrawable;
-    List<ContentModel> items = shapeGroup.getItems();
-    if (items.isEmpty()) {
-      return;
-    }
+    this(lottieDrawable, layer, shapeGroup.getName(),
+        contentsFromModels(lottieDrawable, layer, shapeGroup.getItems()),
+        findTransform(shapeGroup.getItems()));
+  }
 
-    Object potentialTransform = items.get(items.size() - 1);
-    if (potentialTransform instanceof AnimatableTransform) {
-      transformAnimation = ((AnimatableTransform) potentialTransform).createAnimation();
+  ContentGroup(final LottieDrawable lottieDrawable, BaseLayer layer,
+      String name, List<Content> contents, @Nullable AnimatableTransform transform) {
+    this.name = name;
+    this.lottieDrawable = lottieDrawable;
+    this.contents = contents;
+
+    if (transform != null) {
+      transformAnimation = transform.createAnimation();
       transformAnimation.addAnimationsToLayer(layer);
       transformAnimation.addListener(this);
     }
 
     List<GreedyContent> greedyContents = new ArrayList<>();
-    for (int i = 0; i < items.size(); i++) {
-      Content content = items.get(i).toContent(lottieDrawable, layer);
-      if (content != null) {
-        contents.add(content);
-        if (content instanceof GreedyContent) {
-          greedyContents.add((GreedyContent) content);
-        }
+    for (int i = contents.size() - 1; i >= 0; i--) {
+      Content content = contents.get(i);
+      if (content instanceof GreedyContent) {
+        greedyContents.add((GreedyContent) content);
       }
     }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/Repeater.java b/lottie/src/main/java/com/airbnb/lottie/Repeater.java
new file mode 100644
index 0000000..3364ab0
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/Repeater.java
@@ -0,0 +1,58 @@
+package com.airbnb.lottie;
+
+import android.support.annotation.Nullable;
+
+import org.json.JSONObject;
+
+class Repeater implements ContentModel {
+  private final String name;
+  private final AnimatableFloatValue copies;
+  private final AnimatableFloatValue offset;
+  private final AnimatableTransform transform;
+
+  Repeater(String name, AnimatableFloatValue copies, AnimatableFloatValue offset,
+      AnimatableTransform transform) {
+    this.name = name;
+    this.copies = copies;
+    this.offset = offset;
+    this.transform = transform;
+  }
+
+  String getName() {
+    return name;
+  }
+
+  AnimatableFloatValue getCopies() {
+    return copies;
+  }
+
+  AnimatableFloatValue getOffset() {
+    return offset;
+  }
+
+  AnimatableTransform getTransform() {
+    return transform;
+  }
+
+  @Nullable @Override public Content toContent(LottieDrawable drawable, BaseLayer layer) {
+    return new RepeaterContent(drawable, layer, this);
+  }
+
+  final static class Factory {
+
+    private Factory() {
+    }
+
+    static Repeater newInstance(JSONObject json, LottieComposition composition) {
+      String name = json.optString("nm");
+      AnimatableFloatValue copies =
+          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("c"), composition, false);
+      AnimatableFloatValue offset =
+          AnimatableFloatValue.Factory.newInstance(json.optJSONObject("o"), composition, false);
+      AnimatableTransform transform =
+          AnimatableTransform.Factory.newInstance(json.optJSONObject("tr"), composition);
+
+      return new Repeater(name, copies, offset, transform);
+    }
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/RepeaterContent.java b/lottie/src/main/java/com/airbnb/lottie/RepeaterContent.java
new file mode 100644
index 0000000..1af0f6c
--- /dev/null
+++ b/lottie/src/main/java/com/airbnb/lottie/RepeaterContent.java
@@ -0,0 +1,122 @@
+package com.airbnb.lottie;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+public class RepeaterContent implements
+    DrawingContent, PathContent, GreedyContent, BaseKeyframeAnimation.AnimationListener {
+  private final Matrix matrix = new Matrix();
+  private final Path path = new Path();
+
+  private final LottieDrawable lottieDrawable;
+  private final BaseLayer layer;
+  private final String name;
+  private final KeyframeAnimation<Float> copies;
+  private final KeyframeAnimation<Float> offset;
+  private final TransformKeyframeAnimation transform;
+  private ContentGroup contentGroup;
+
+
+  RepeaterContent(LottieDrawable lottieDrawable, BaseLayer layer, Repeater repeater) {
+    this.lottieDrawable = lottieDrawable;
+    this.layer = layer;
+    name = repeater.getName();
+    copies = repeater.getCopies().createAnimation();
+    layer.addAnimation(copies);
+    copies.addUpdateListener(this);
+
+    offset = repeater.getOffset().createAnimation();
+    layer.addAnimation(offset);
+    offset.addUpdateListener(this);
+
+    transform = repeater.getTransform().createAnimation();
+    transform.addAnimationsToLayer(layer);
+    transform.addListener(this);
+  }
+
+  @Override public void absorbContent(ListIterator<Content> contentsIter) {
+    // This check prevents a repeater from getting added twice.
+    // This can happen in the following situation:
+    //    RECTANGLE
+    //    REPEATER 1
+    //    FILL
+    //    REPEATER 2
+    // In this case, the expected structure would be:
+    //     REPEATER 2
+    //        REPEATER 1
+    //            RECTANGLE
+    //        FILL
+    // Without this check, REPEATER 1 will try and absorb contents once it is already inside of
+    // REPEATER 2.
+    if (contentGroup != null) {
+      return;
+    }
+    // Fast forward the iterator until after this content.
+    //noinspection StatementWithEmptyBody
+    while (contentsIter.hasPrevious() && contentsIter.previous() != this) {}
+    List<Content> contents = new ArrayList<>();
+    while (contentsIter.hasPrevious()) {
+      contents.add(contentsIter.previous());
+      contentsIter.remove();
+    }
+    Collections.reverse(contents);
+    contentGroup = new ContentGroup(lottieDrawable, layer, "Repeater", contents, null);
+  }
+
+  @Override public String getName() {
+    return name;
+  }
+
+  @Override public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
+    contentGroup.setContents(contentsBefore, contentsAfter);
+  }
+
+  @Override public Path getPath() {
+    Path contentPath = contentGroup.getPath();
+    path.reset();
+    float copies = this.copies.getValue();
+    float offset = this.offset.getValue();
+    for (int i = (int) copies - 1; i >= 0; i--) {
+      matrix.set(transform.getMatrixForRepeater(i + offset));
+      path.addPath(contentPath, matrix);
+    }
+    return path;
+  }
+
+  @Override public void draw(Canvas canvas, Matrix parentMatrix, int alpha) {
+    float copies = this.copies.getValue();
+    float offset = this.offset.getValue();
+    //noinspection ConstantConditions
+    float startOpacity = this.transform.getStartOpacity().getValue() / 100f;
+    //noinspection ConstantConditions
+    float endOpacity = this.transform.getEndOpacity().getValue() / 100f;
+    for (int i = (int) copies - 1; i >= 0; i--) {
+      matrix.set(parentMatrix);
+      matrix.preConcat(transform.getMatrixForRepeater(i + offset));
+      float newAlpha = alpha * MiscUtils.lerp(startOpacity, endOpacity, i / copies);
+      contentGroup.draw(canvas, matrix, (int) newAlpha);
+    }
+  }
+
+  @Override public void getBounds(RectF outBounds, Matrix parentMatrix) {
+    contentGroup.getBounds(outBounds, parentMatrix);
+  }
+
+  @Override public void addColorFilter(@Nullable String layerName, @Nullable String contentName,
+      @Nullable ColorFilter colorFilter) {
+    contentGroup.addColorFilter(layerName, contentName, colorFilter);
+  }
+
+  @Override public void onValueChanged() {
+    lottieDrawable.invalidateSelf();
+  }
+}
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java b/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
index 30875a4..b8486c4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
@@ -39,6 +39,8 @@
         return PolystarShape.Factory.newInstance(json, composition);
       case "mm":
         return MergePaths.Factory.newInstance(json);
+      case "rp":
+        return Repeater.Factory.newInstance(json, composition);
       default:
         Log.w(L.TAG, "Unknown shape type " + type);
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/TransformKeyframeAnimation.java b/lottie/src/main/java/com/airbnb/lottie/TransformKeyframeAnimation.java
index cb5bef4..466ba70 100644
--- a/lottie/src/main/java/com/airbnb/lottie/TransformKeyframeAnimation.java
+++ b/lottie/src/main/java/com/airbnb/lottie/TransformKeyframeAnimation.java
@@ -2,15 +2,20 @@
 
 import android.graphics.Matrix;
 import android.graphics.PointF;
+import android.support.annotation.Nullable;
 
 class TransformKeyframeAnimation {
   private final Matrix matrix = new Matrix();
 
-  private final BaseKeyframeAnimation<?, PointF> anchorPoint;
+  private final KeyframeAnimation<PointF> anchorPoint;
   private final BaseKeyframeAnimation<?, PointF> position;
-  private final BaseKeyframeAnimation<?, ScaleXY> scale;
-  private final BaseKeyframeAnimation<?, Float> rotation;
-  private final BaseKeyframeAnimation<?, Integer> opacity;
+  private final KeyframeAnimation<ScaleXY> scale;
+  private final KeyframeAnimation<Float> rotation;
+  private final KeyframeAnimation<Integer> opacity;
+
+  // Used for repeaters
+  @Nullable private final BaseKeyframeAnimation<?, Float> startOpacity;
+  @Nullable private final BaseKeyframeAnimation<?, Float> endOpacity;
 
   TransformKeyframeAnimation(AnimatableTransform animatableTransform) {
     anchorPoint = animatableTransform.getAnchorPoint().createAnimation();
@@ -18,6 +23,16 @@
     scale = animatableTransform.getScale().createAnimation();
     rotation = animatableTransform.getRotation().createAnimation();
     opacity = animatableTransform.getOpacity().createAnimation();
+    if (animatableTransform.getStartOpacity() != null) {
+      startOpacity = animatableTransform.getStartOpacity().createAnimation();
+    } else {
+      startOpacity = null;
+    }
+    if (animatableTransform.getEndOpacity() != null) {
+      endOpacity = animatableTransform.getEndOpacity().createAnimation();
+    } else {
+      endOpacity = null;
+    }
   }
 
   void addAnimationsToLayer(BaseLayer layer) {
@@ -26,6 +41,12 @@
     layer.addAnimation(scale);
     layer.addAnimation(rotation);
     layer.addAnimation(opacity);
+    if (startOpacity != null) {
+      layer.addAnimation(startOpacity);
+    }
+    if (endOpacity != null) {
+      layer.addAnimation(endOpacity);
+    }
   }
 
   void addListener(final BaseKeyframeAnimation.AnimationListener listener) {
@@ -34,12 +55,27 @@
     scale.addUpdateListener(listener);
     rotation.addUpdateListener(listener);
     opacity.addUpdateListener(listener);
+    if (startOpacity != null) {
+      startOpacity.addUpdateListener(listener);
+    }
+    if (endOpacity != null) {
+      endOpacity.addUpdateListener(listener);
+    }
   }
 
   BaseKeyframeAnimation<?, Integer> getOpacity() {
     return opacity;
   }
 
+  @Nullable public BaseKeyframeAnimation<?, Float> getStartOpacity() {
+    return startOpacity;
+  }
+
+  @Nullable public BaseKeyframeAnimation<?, Float> getEndOpacity() {
+    return endOpacity;
+  }
+
+
   Matrix getMatrix() {
     matrix.reset();
     PointF position = this.position.getValue();
@@ -63,4 +99,24 @@
     }
     return matrix;
   }
+
+  /**
+   * TODO: see if we can use this for the main {@link #getMatrix()} method.
+   */
+  Matrix getMatrixForRepeater(float amount) {
+    PointF position = this.position.getValue();
+    PointF anchorPoint = this.anchorPoint.getValue();
+    ScaleXY scale = this.scale.getValue();
+    float rotation = this.rotation.getValue();
+
+    matrix.reset();
+    matrix.preTranslate(position.x * amount, position.y * amount);
+    matrix.preScale(
+        (float) Math.pow(scale.getScaleX(), amount),
+         (float) Math.pow(scale.getScaleY(), amount));
+    matrix.preRotate(rotation * amount, anchorPoint.x, anchorPoint.y);
+
+    return matrix;
+  }
+
 }