Extract more factory classes (#139)

Another change for #3

Pretty mechanical and straightforward change. Extracted a bunch of constructors into more static inner factory classes with a newInstance method by convention.
Later on we can pull out all these factories into a separate module that provides the default json parsing implementation.
diff --git a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
index 4fead43..a7ed1ad 100644
--- a/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
+++ b/LottieSample/src/androidTest/java/com/airbnb/lottie/LottieTest.java
@@ -22,34 +22,35 @@
       MainActivity.class);
 
   @Test public void testAll() {
-    TestRobot.testAnimation(activityRule.getActivity(), "9squares-AlBoardman.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "EmptyState.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "HamburgerArrow.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "LottieLogo1.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "LottieLogo2.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "MotionCorpse-Jrcanest.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "PinJump.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "TwitterHeart.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Hosts.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/LightBulb.json", null,
+    MainActivity activity = activityRule.getActivity();
+    TestRobot.testAnimation(activity, "9squares-AlBoardman.json");
+    TestRobot.testAnimation(activity, "EmptyState.json");
+    TestRobot.testAnimation(activity, "HamburgerArrow.json");
+    TestRobot.testAnimation(activity, "LottieLogo1.json");
+    TestRobot.testAnimation(activity, "LottieLogo2.json");
+    TestRobot.testAnimation(activity, "MotionCorpse-Jrcanest.json");
+    TestRobot.testAnimation(activity, "PinJump.json");
+    TestRobot.testAnimation(activity, "TwitterHeart.json");
+    TestRobot.testAnimation(activity, "Tests/Hosts.json");
+    TestRobot.testAnimation(activity, "Tests/LightBulb.json", null,
         new float[]{0f, 0.05f, 0.10f, 0.2f, 0.3f, 0.4f, 0.5f, 1f});
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/LoopPlayOnce.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Alarm.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/CheckSwitch.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/EllipseTrimPath.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/SplitDimensions.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/TrimPathsFull.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Laugh4.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Star.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Polygon.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/AllSet.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/City.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/PreCompMadness.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/MatteParentPrecomp.json");
-    TestRobot.testAnimation(activityRule.getActivity(), "Tests/Image.json", "Tests/weaccept");
-    TestRobot.testStatic(activityRule.getActivity(), "Tests/TrimPathFill.json");
-    TestRobot.testStatic(activityRule.getActivity(), "Tests/Mask_26.json");
-    TestRobot.testStatic(activityRule.getActivity(), "Tests/MatteInv.json");
-    TestRobot.testStatic(activityRule.getActivity(), "Tests/MaskInv.json");
+    TestRobot.testAnimation(activity, "Tests/LoopPlayOnce.json");
+    TestRobot.testAnimation(activity, "Tests/Alarm.json");
+    TestRobot.testAnimation(activity, "Tests/CheckSwitch.json");
+    TestRobot.testAnimation(activity, "Tests/EllipseTrimPath.json");
+    TestRobot.testAnimation(activity, "Tests/SplitDimensions.json");
+    TestRobot.testAnimation(activity, "Tests/TrimPathsFull.json");
+    TestRobot.testAnimation(activity, "Tests/Laugh4.json");
+    TestRobot.testAnimation(activity, "Tests/Star.json");
+    TestRobot.testAnimation(activity, "Tests/Polygon.json");
+    TestRobot.testAnimation(activity, "Tests/AllSet.json");
+    TestRobot.testAnimation(activity, "Tests/City.json");
+    TestRobot.testAnimation(activity, "Tests/PreCompMadness.json");
+    TestRobot.testAnimation(activity, "Tests/MatteParentPrecomp.json");
+    TestRobot.testAnimation(activity, "Tests/Image.json", "Tests/weaccept");
+    TestRobot.testStatic(activity, "Tests/TrimPathFill.json");
+    TestRobot.testStatic(activity, "Tests/Mask_26.json");
+    TestRobot.testStatic(activity, "Tests/MatteInv.json");
+    TestRobot.testStatic(activity, "Tests/MaskInv.json");
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java b/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
index 29bd13f..cf3d67e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
+++ b/lottie/src/main/java/com/airbnb/lottie/AnimatableTransform.java
@@ -3,63 +3,81 @@
 import org.json.JSONObject;
 
 class AnimatableTransform {
-  private AnimatablePathValue anchorPoint;
-  private IAnimatablePathValue position;
-  private AnimatableScaleValue scale;
-  private AnimatableFloatValue rotation;
-  private AnimatableIntegerValue opacity;
+  private final AnimatablePathValue anchorPoint;
+  private final IAnimatablePathValue position;
+  private final AnimatableScaleValue scale;
+  private final AnimatableFloatValue rotation;
+  private final AnimatableIntegerValue opacity;
 
-  AnimatableTransform(LottieComposition composition) {
-    this.anchorPoint = new AnimatablePathValue();
-    this.position = new AnimatablePathValue();
-    this.scale = new AnimatableScaleValue(composition);
-    this.rotation = new AnimatableFloatValue(composition, 0f);
-    this.opacity = new AnimatableIntegerValue(composition, 255);
+  AnimatableTransform(AnimatablePathValue anchorPoint, IAnimatablePathValue position,
+      AnimatableScaleValue scale, AnimatableFloatValue rotation, AnimatableIntegerValue opacity) {
+    this.anchorPoint = anchorPoint;
+    this.position = position;
+    this.scale = scale;
+    this.rotation = rotation;
+    this.opacity = opacity;
   }
 
-  AnimatableTransform(JSONObject json, LottieComposition composition) {
-    JSONObject anchorJson = json.optJSONObject("a");
-    if (anchorJson != null) {
-      anchorPoint = new AnimatablePathValue(anchorJson.opt("k"), composition);
-    } else {
-      throwMissingTransform("anchor");
+  static class Factory {
+    static AnimatableTransform newInstance(LottieComposition composition) {
+      AnimatablePathValue anchorPoint = new AnimatablePathValue();
+      IAnimatablePathValue position = new AnimatablePathValue();
+      AnimatableScaleValue scale = new AnimatableScaleValue(composition);
+      AnimatableFloatValue rotation = new AnimatableFloatValue(composition, 0f);
+      AnimatableIntegerValue opacity = new AnimatableIntegerValue(composition, 255);
+      return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity);
     }
 
-    JSONObject positionJson = json.optJSONObject("p");
-    if (positionJson != null) {
-      position =
-          AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(positionJson, composition);
-    } else {
-      throwMissingTransform("position");
+    static AnimatableTransform newInstance(JSONObject json, LottieComposition composition) {
+      AnimatablePathValue anchorPoint = null;
+      IAnimatablePathValue position = null;
+      AnimatableScaleValue scale = null;
+      AnimatableFloatValue rotation = null;
+      AnimatableIntegerValue opacity = null;
+      JSONObject anchorJson = json.optJSONObject("a");
+      if (anchorJson != null) {
+        anchorPoint = new AnimatablePathValue(anchorJson.opt("k"), composition);
+      } else {
+        throwMissingTransform("anchor");
+      }
+
+      JSONObject positionJson = json.optJSONObject("p");
+      if (positionJson != null) {
+        position =
+            AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(positionJson, composition);
+      } else {
+        throwMissingTransform("position");
+      }
+
+      JSONObject scaleJson = json.optJSONObject("s");
+      if (scaleJson != null) {
+        scale = new AnimatableScaleValue(scaleJson, composition, false);
+      } else {
+        throwMissingTransform("scale");
+      }
+
+      JSONObject rotationJson = json.optJSONObject("r");
+      if (rotationJson == null) {
+        rotationJson = json.optJSONObject("rz");
+      }
+      if (rotationJson != null) {
+        rotation = new AnimatableFloatValue(rotationJson, composition, false);
+      } else {
+        throwMissingTransform("rotation");
+      }
+
+      JSONObject opacityJson = json.optJSONObject("o");
+      if (opacityJson != null) {
+        opacity = new AnimatableIntegerValue(opacityJson, composition, false, true);
+      } else {
+        throwMissingTransform("opacity");
+      }
+      return new AnimatableTransform(anchorPoint, position, scale, rotation, opacity);
     }
 
-    JSONObject scaleJson = json.optJSONObject("s");
-    if (scaleJson != null) {
-      scale = new AnimatableScaleValue(scaleJson, composition, false);
-    } else {
-      throwMissingTransform("scale");
+    private static void throwMissingTransform(String missingProperty) {
+      throw new IllegalArgumentException("Missing transform for " + missingProperty);
     }
-
-    JSONObject rotationJson = json.optJSONObject("r");
-    if (rotationJson == null) {
-      rotationJson = json.optJSONObject("rz");
-    }
-    if (rotationJson != null) {
-      rotation = new AnimatableFloatValue(rotationJson, composition, false);
-    } else {
-      throwMissingTransform("rotation");
-    }
-
-    JSONObject opacityJson = json.optJSONObject("o");
-    if (opacityJson != null) {
-      opacity = new AnimatableIntegerValue(opacityJson, composition, false, true);
-    } else {
-      throwMissingTransform("opacity");
-    }
-  }
-
-  private void throwMissingTransform(String missingProperty) {
-    throw new IllegalArgumentException("Missing transform for " + missingProperty);
   }
 
   AnimatablePathValue getAnchorPoint() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/CircleShape.java b/lottie/src/main/java/com/airbnb/lottie/CircleShape.java
index 9cbf300..7d659af 100644
--- a/lottie/src/main/java/com/airbnb/lottie/CircleShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/CircleShape.java
@@ -6,10 +6,18 @@
   private final IAnimatablePathValue position;
   private final AnimatablePointValue size;
 
-  CircleShape(JSONObject json, LottieComposition composition) {
-    position = AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
-        json.optJSONObject("p"), composition);
-    size = new AnimatablePointValue(json.optJSONObject("s"), composition);
+  private CircleShape(IAnimatablePathValue position, AnimatablePointValue size) {
+    this.position = position;
+    this.size = size;
+  }
+
+  static class Factory {
+    static CircleShape newInstance(JSONObject json, LottieComposition composition) {
+      return new CircleShape(
+          AnimatablePathValue
+              .createAnimatablePathOrSplitDimensionPath(json.optJSONObject("p"), composition),
+          new AnimatablePointValue(json.optJSONObject("s"), composition));
+    }
   }
 
   public IAnimatablePathValue getPosition() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java
index 244c7ca..077314b 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ImageAsset.java
@@ -3,11 +3,10 @@
 import org.json.JSONObject;
 
 class ImageAsset {
-
-  private int width;
-  private int height;
-  private String id;
-  private String fileName;
+  private final int width;
+  private final int height;
+  private final String id;
+  private final String fileName;
 
   ImageAsset(JSONObject imageJson) {
     width = imageJson.optInt("w");
diff --git a/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java b/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
index 45a199a..687b5f6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ImageAssetBitmapManager.java
@@ -16,7 +16,6 @@
 import static junit.framework.Assert.assertNotNull;
 
 class ImageAssetBitmapManager {
-
   private final Context context;
   private String imagesFolder;
   private final Map<String, ImageAsset> imageAssets;
diff --git a/lottie/src/main/java/com/airbnb/lottie/Keyframe.java b/lottie/src/main/java/com/airbnb/lottie/Keyframe.java
index 609e54c..bf1b6c4 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Keyframe.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Keyframe.java
@@ -15,7 +15,7 @@
 import java.util.List;
 
 public class Keyframe<T> {
-  private static Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+  private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
 
   static <T> List<Keyframe<T>> parseKeyframes(JSONArray json, LottieComposition composition,
       float scale, AnimatableValue<T, ?> animatableValue) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/Layer.java b/lottie/src/main/java/com/airbnb/lottie/Layer.java
index 15ee77d..9ae24d5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Layer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Layer.java
@@ -31,116 +31,48 @@
     Unknown
   }
 
-  private final List<Object> shapes = new ArrayList<>();
+  private final List<Object> shapes;
   private final LottieComposition composition;
-
   private final String layerName;
   private final long layerId;
   private final LayerType layerType;
   private final long parentId;
   @Nullable private final String refId;
-
-  private final List<Mask> masks = new ArrayList<>();
-
+  private final List<Mask> masks;
   private final AnimatableTransform transform;
-  private int solidWidth;
-  private int solidHeight;
-  private int solidColor;
-
+  private final int solidWidth;
+  private final int solidHeight;
+  private final int solidColor;
   private final float timeStretch;
   private final float startProgress;
-  private float preCompStartProgress;
-  private int preCompWidth;
-  private int preCompHeight;
-  @Nullable private ImageAsset imageAsset;
+  private final int preCompWidth;
+  private final int preCompHeight;
+  private final List<Keyframe<Float>> inOutKeyframes;
+  private final MatteType matteType;
 
-  private final List<Keyframe<Float>> inOutKeyframes = new ArrayList<>();
-
-  private MatteType matteType;
-
-  Layer(JSONObject json, LottieComposition composition) {
+  private Layer(List<Object> shapes, LottieComposition composition, String layerName, long layerId,
+      LayerType layerType, long parentId, @Nullable String refId, List<Mask> masks,
+      AnimatableTransform transform, int solidWidth, int solidHeight, int solidColor,
+      float timeStretch, float startProgress, int preCompWidth, int preCompHeight,
+      List<Keyframe<Float>> inOutKeyframes, MatteType matteType) {
+    this.shapes = shapes;
     this.composition = composition;
-    layerName = json.optString("nm");
-    layerId = json.optLong("ind");
-    refId = json.optString("refId");
-
-    int layerTypeInt = json.optInt("ty", -1);
-    if (layerTypeInt < LayerType.Unknown.ordinal()) {
-      layerType = LayerType.values()[layerTypeInt];
-    } else {
-      layerType = LayerType.Unknown;
-    }
-
-    parentId = json.optLong("parent", -1);
-
-    if (layerType == LayerType.Solid) {
-      solidWidth = (int) (json.optInt("sw") * composition.getScale());
-      solidHeight = (int) (json.optInt("sh") * composition.getScale());
-      solidColor = Color.parseColor(json.optString("sc"));
-      if (L.DBG) {
-        Log.d(TAG, "\tSolid=" + Integer.toHexString(solidColor) + " " +
-            solidWidth + "x" + solidHeight + " " + composition.getBounds());
-      }
-    }
-
-    transform = new AnimatableTransform(json.optJSONObject("ks"), composition);
-
-    matteType = MatteType.values()[json.optInt("tt")];
-
-    JSONArray jsonMasks = json.optJSONArray("masksProperties");
-    if (jsonMasks != null) {
-      for (int i = 0; i < jsonMasks.length(); i++) {
-        Mask mask = new Mask(jsonMasks.optJSONObject(i), composition);
-        masks.add(mask);
-      }
-    }
-
-    JSONArray shapesJson = json.optJSONArray("shapes");
-    if (shapesJson != null) {
-      for (int i = 0; i < shapesJson.length(); i++) {
-        Object shape = ShapeGroup.shapeItemWithJson(shapesJson.optJSONObject(i), composition);
-        if (shape != null) {
-          shapes.add(shape);
-        }
-      }
-    }
-
-    timeStretch = (float) json.optDouble("sr", 1.0);
-    float startFrame = (float) json.optDouble("st");
-    float frames = composition.getDurationFrames();
-    startProgress = startFrame / frames;
-
-    if (layerType == LayerType.PreComp) {
-      preCompWidth = (int) (json.optInt("w") * composition.getScale());
-      preCompHeight = (int) (json.optInt("h") * composition.getScale());
-    }
-
-    long inFrame = json.optLong("ip");
-    long outFrame = json.optLong("op");
-
-    // Before the in frame
-    if (inFrame > 0) {
-      Keyframe<Float> preKeyframe = new Keyframe<>(composition, 0, inFrame);
-      preKeyframe.startValue = 0f;
-      preKeyframe.endValue = 0f;
-      inOutKeyframes.add(preKeyframe);
-    }
-
-    // The + 1 is because the animation should be visible on the out frame itself.
-    outFrame = (outFrame > 0 ? outFrame : composition.getEndFrame() + 1);
-    Keyframe<Float> visibleKeyframe =
-        new Keyframe<>(composition, inFrame, outFrame);
-    visibleKeyframe.startValue = 1f;
-    visibleKeyframe.endValue = 1f;
-    inOutKeyframes.add(visibleKeyframe);
-
-    if (outFrame <= composition.getDurationFrames()) {
-      Keyframe<Float> outKeyframe =
-          new Keyframe<>(composition, outFrame, composition.getEndFrame());
-      outKeyframe.startValue = 0f;
-      outKeyframe.endValue = 0f;
-      inOutKeyframes.add(outKeyframe);
-    }
+    this.layerName = layerName;
+    this.layerId = layerId;
+    this.layerType = layerType;
+    this.parentId = parentId;
+    this.refId = refId;
+    this.masks = masks;
+    this.transform = transform;
+    this.solidWidth = solidWidth;
+    this.solidHeight = solidHeight;
+    this.solidColor = solidColor;
+    this.timeStretch = timeStretch;
+    this.startProgress = startProgress;
+    this.preCompWidth = preCompWidth;
+    this.preCompHeight = preCompHeight;
+    this.inOutKeyframes = inOutKeyframes;
+    this.matteType = matteType;
   }
 
   LottieComposition getComposition() {
@@ -247,4 +179,101 @@
     }
     return sb.toString();
   }
+
+  static class Factory {
+    static Layer newInstance(JSONObject json, LottieComposition composition) {
+      String layerName = json.optString("nm");
+      String refId = json.optString("refId");
+      long layerId = json.optLong("ind");
+      int solidWidth = 0;
+      int solidHeight = 0;
+      int solidColor = 0;
+      int preCompWidth = 0;
+      int preCompHeight = 0;
+      LayerType layerType;
+      int layerTypeInt = json.optInt("ty", -1);
+      if (layerTypeInt < LayerType.Unknown.ordinal()) {
+        layerType = LayerType.values()[layerTypeInt];
+      } else {
+        layerType = LayerType.Unknown;
+      }
+
+      long parentId = json.optLong("parent", -1);
+
+      if (layerType == LayerType.Solid) {
+        solidWidth = (int) (json.optInt("sw") * composition.getScale());
+        solidHeight = (int) (json.optInt("sh") * composition.getScale());
+        solidColor = Color.parseColor(json.optString("sc"));
+        if (L.DBG) {
+          Log.d(TAG, "\tSolid=" + Integer.toHexString(solidColor) + " " +
+              solidWidth + "x" + solidHeight + " " + composition.getBounds());
+        }
+      }
+
+      AnimatableTransform transform = AnimatableTransform.Factory.newInstance(json.optJSONObject("ks"),
+          composition);
+      MatteType matteType = MatteType.values()[json.optInt("tt")];
+      List<Object> shapes = new ArrayList<>();
+      List<Mask> masks = new ArrayList<>();
+      List<Keyframe<Float>> inOutKeyframes = new ArrayList<>();
+      JSONArray jsonMasks = json.optJSONArray("masksProperties");
+      if (jsonMasks != null) {
+        for (int i = 0; i < jsonMasks.length(); i++) {
+          Mask mask = Mask.Factory.newMask(jsonMasks.optJSONObject(i), composition);
+          masks.add(mask);
+        }
+      }
+
+      JSONArray shapesJson = json.optJSONArray("shapes");
+      if (shapesJson != null) {
+        for (int i = 0; i < shapesJson.length(); i++) {
+          Object shape = ShapeGroup.shapeItemWithJson(shapesJson.optJSONObject(i), composition);
+          if (shape != null) {
+            shapes.add(shape);
+          }
+        }
+      }
+
+      float timeStretch = (float) json.optDouble("sr", 1.0);
+      float startFrame = (float) json.optDouble("st");
+      float frames = composition.getDurationFrames();
+      float startProgress = startFrame / frames;
+
+      if (layerType == LayerType.PreComp) {
+        preCompWidth = (int) (json.optInt("w") * composition.getScale());
+        preCompHeight = (int) (json.optInt("h") * composition.getScale());
+      }
+
+      long inFrame = json.optLong("ip");
+      long outFrame = json.optLong("op");
+
+      // Before the in frame
+      if (inFrame > 0) {
+        Keyframe<Float> preKeyframe = new Keyframe<>(composition, 0, inFrame);
+        preKeyframe.startValue = 0f;
+        preKeyframe.endValue = 0f;
+        inOutKeyframes.add(preKeyframe);
+      }
+
+      // The + 1 is because the animation should be visible on the out frame itself.
+      outFrame = (outFrame > 0 ? outFrame : composition.getEndFrame() + 1);
+      Keyframe<Float> visibleKeyframe =
+          new Keyframe<>(composition, inFrame, outFrame);
+      visibleKeyframe.startValue = 1f;
+      visibleKeyframe.endValue = 1f;
+      inOutKeyframes.add(visibleKeyframe);
+
+      if (outFrame <= composition.getDurationFrames()) {
+        Keyframe<Float> outKeyframe =
+            new Keyframe<>(composition, outFrame, composition.getEndFrame());
+        outKeyframe.startValue = 0f;
+        outKeyframe.endValue = 0f;
+        inOutKeyframes.add(outKeyframe);
+      }
+
+      return new Layer(shapes, composition, layerName, layerId, layerType, parentId, refId,
+          masks, transform, solidWidth, solidHeight, solidColor, timeStretch, startProgress,
+          preCompWidth, preCompHeight, inOutKeyframes, matteType);
+    }
+  }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LayerView.java b/lottie/src/main/java/com/airbnb/lottie/LayerView.java
index 082551b..e2b7c90 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LayerView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LayerView.java
@@ -138,24 +138,24 @@
         ShapePath shapePath = (ShapePath) item;
         ShapeLayerView shapeLayer =
             new ShapeLayerView(shapePath, currentFill, currentStroke, currentTrim,
-                new AnimatableTransform(composition), getCallback());
+                AnimatableTransform.Factory.newInstance(composition), getCallback());
         addLayer(shapeLayer);
       } else if (item instanceof RectangleShape) {
         RectangleShape shapeRect = (RectangleShape) item;
         RectLayer shapeLayer =
             new RectLayer(shapeRect, currentFill, currentStroke, currentTrim,
-                new AnimatableTransform(composition), getCallback());
+                AnimatableTransform.Factory.newInstance(composition), getCallback());
         addLayer(shapeLayer);
       } else if (item instanceof CircleShape) {
         CircleShape shapeCircle = (CircleShape) item;
         EllipseLayer shapeLayer =
             new EllipseLayer(shapeCircle, currentFill, currentStroke, currentTrim,
-                new AnimatableTransform(composition), getCallback());
+                AnimatableTransform.Factory.newInstance(composition), getCallback());
         addLayer(shapeLayer);
       } else if (item instanceof PolystarShape) {
         PolystarShape polystarShape = (PolystarShape) item;
         PolystarLayer shapeLayer = new PolystarLayer(polystarShape, currentFill, currentStroke,
-            currentTrim, new AnimatableTransform(composition), getCallback());
+            currentTrim, AnimatableTransform.Factory.newInstance(composition), getCallback());
         addLayer(shapeLayer);
       }
     }
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
index 1497774..4105b9f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieComposition.java
@@ -134,13 +134,13 @@
     }
 
     static LottieComposition fromFileSync(Context context, String fileName) {
-      InputStream file;
+      InputStream stream;
       try {
-        file = context.getAssets().open(fileName);
+        stream = context.getAssets().open(fileName);
       } catch (IOException e) {
         throw new IllegalStateException("Unable to find file " + fileName, e);
       }
-      return fromInputStream(context.getResources(), file);
+      return fromInputStream(context.getResources(), stream);
     }
 
     /**
@@ -209,7 +209,7 @@
       JSONArray jsonLayers = json.optJSONArray("layers");
       int length = jsonLayers.length();
       for (int i = 0; i < length; i++) {
-        Layer layer = new Layer(jsonLayers.optJSONObject(i), composition);
+        Layer layer = Layer.Factory.newInstance(jsonLayers.optJSONObject(i), composition);
         addLayer(composition.layers, composition.layerMap, layer);
       }
     }
@@ -225,7 +225,7 @@
         List<Layer> layers = new ArrayList<>(layersJson.length());
         LongSparseArray<Layer> layerMap = new LongSparseArray<>();
         for (int j = 0; j < layersJson.length(); j++) {
-          Layer layer = new Layer(layersJson.optJSONObject(j), composition);
+          Layer layer = Layer.Factory.newInstance(layersJson.optJSONObject(j), composition);
           layerMap.put(layer.getId(), layer);
           layers.add(layer);
         }
diff --git a/lottie/src/main/java/com/airbnb/lottie/Mask.java b/lottie/src/main/java/com/airbnb/lottie/Mask.java
index 0feb6b3..8eb8ba8 100644
--- a/lottie/src/main/java/com/airbnb/lottie/Mask.java
+++ b/lottie/src/main/java/com/airbnb/lottie/Mask.java
@@ -13,31 +13,40 @@
   private final MaskMode maskMode;
   private final AnimatableShapeValue maskPath;
 
-  Mask(JSONObject json, LottieComposition composition) {
-    String mode = json.optString("mode");
-    switch (mode) {
-      case "a":
-        maskMode = MaskMode.MaskModeAdd;
-        break;
-      case "s":
-        maskMode = MaskMode.MaskModeSubtract;
-        break;
-      case "i":
-        maskMode = MaskMode.MaskModeIntersect;
-        break;
-      default:
-        maskMode = MaskMode.MaskModeUnknown;
-    }
-
-    maskPath = new AnimatableShapeValue(json.optJSONObject("pt"), composition);
-    // TODO: use this
-    // JSONObject opacityJson = json.optJSONObject("o");
-    // if (opacityJson != null) {
-    //   AnimatableIntegerValue opacity =
-    //       new AnimatableIntegerValue(opacityJson, composition, false, true);
-    // }
+  private Mask(MaskMode maskMode, AnimatableShapeValue maskPath) {
+    this.maskMode = maskMode;
+    this.maskPath = maskPath;
   }
 
+  static class Factory {
+    static Mask newMask(JSONObject json, LottieComposition composition) {
+      MaskMode maskMode;
+      switch (json.optString("mode")) {
+        case "a":
+          maskMode = MaskMode.MaskModeAdd;
+          break;
+        case "s":
+          maskMode = MaskMode.MaskModeSubtract;
+          break;
+        case "i":
+          maskMode = MaskMode.MaskModeIntersect;
+          break;
+        default:
+          maskMode = MaskMode.MaskModeUnknown;
+      }
+
+      AnimatableShapeValue maskPath = new AnimatableShapeValue(json.optJSONObject("pt"),
+          composition);
+      // TODO: use this
+      // JSONObject opacityJson = json.optJSONObject("o");
+      // if (opacityJson != null) {
+      //   AnimatableIntegerValue opacity =
+      //       new AnimatableIntegerValue(opacityJson, composition, false, true);
+      // }
+
+      return new Mask(maskMode, maskPath);
+    }
+  }
 
   @SuppressWarnings("unused") MaskMode getMaskMode() {
     return maskMode;
diff --git a/lottie/src/main/java/com/airbnb/lottie/PathKeyframe.java b/lottie/src/main/java/com/airbnb/lottie/PathKeyframe.java
index 4d2c617..a186a7b 100644
--- a/lottie/src/main/java/com/airbnb/lottie/PathKeyframe.java
+++ b/lottie/src/main/java/com/airbnb/lottie/PathKeyframe.java
@@ -8,10 +8,8 @@
 import org.json.JSONObject;
 
 class PathKeyframe extends Keyframe<PointF> {
-
   @Nullable private Path path;
 
-
   PathKeyframe(JSONObject json, LottieComposition composition,
       AnimatableValue<PointF, ?> animatableValue) {
     super(json, composition, composition.getScale(), animatableValue);
@@ -29,11 +27,8 @@
     }
   }
 
-  /**
-   * This will be null if the startValue and endValue are the same.
-   */
-  @Nullable
-  Path getPath() {
+  /** This will be null if the startValue and endValue are the same. */
+  @Nullable Path getPath() {
     return path;
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/PolystarShape.java b/lottie/src/main/java/com/airbnb/lottie/PolystarShape.java
index 98b1b91..c646d7f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/PolystarShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/PolystarShape.java
@@ -32,22 +32,45 @@
   private final AnimatableFloatValue innerRoundedness;
   private final AnimatableFloatValue outerRoundedness;
 
-  PolystarShape(JSONObject json, LottieComposition composition) {
-    type = Type.forValue(json.optInt("sy"));
-    points = new AnimatableFloatValue(json.optJSONObject("pt"), composition, false);
-    position = AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
-        json.optJSONObject("p"), composition);
-    rotation = new AnimatableFloatValue(json.optJSONObject("r"), composition, false);
+  private PolystarShape(Type type, AnimatableFloatValue points, IAnimatablePathValue position,
+      AnimatableFloatValue rotation, AnimatableFloatValue innerRadius,
+      AnimatableFloatValue outerRadius, AnimatableFloatValue innerRoundedness,
+      AnimatableFloatValue outerRoundedness) {
+    this.type = type;
+    this.points = points;
+    this.position = position;
+    this.rotation = rotation;
+    this.innerRadius = innerRadius;
+    this.outerRadius = outerRadius;
+    this.innerRoundedness = innerRoundedness;
+    this.outerRoundedness = outerRoundedness;
+  }
 
-    outerRadius = new AnimatableFloatValue(json.optJSONObject("or"), composition);
-    outerRoundedness = new AnimatableFloatValue(json.optJSONObject("os"), composition, false);
+  static class Factory {
+    static PolystarShape newInstance(JSONObject json, LottieComposition composition) {
+      Type type = Type.forValue(json.optInt("sy"));
+      AnimatableFloatValue points =
+          new AnimatableFloatValue(json.optJSONObject("pt"), composition, false);
+      IAnimatablePathValue position = AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
+          json.optJSONObject("p"), composition);
+      AnimatableFloatValue rotation =
+          new AnimatableFloatValue(json.optJSONObject("r"), composition, false);
+      AnimatableFloatValue outerRadius =
+          new AnimatableFloatValue(json.optJSONObject("or"), composition);
+      AnimatableFloatValue outerRoundedness =
+          new AnimatableFloatValue(json.optJSONObject("os"), composition, false);
+      AnimatableFloatValue innerRadius;
+      AnimatableFloatValue innerRoundedness;
 
-    if (type == Type.Star) {
-      innerRadius = new AnimatableFloatValue(json.optJSONObject("ir"), composition);
-      innerRoundedness = new AnimatableFloatValue(json.optJSONObject("is"), composition, false);
-    } else {
-      innerRadius = null;
-      innerRoundedness = null;
+      if (type == Type.Star) {
+        innerRadius = new AnimatableFloatValue(json.optJSONObject("ir"), composition);
+        innerRoundedness = new AnimatableFloatValue(json.optJSONObject("is"), composition, false);
+      } else {
+        innerRadius = null;
+        innerRoundedness = null;
+      }
+      return new PolystarShape(type, points, position, rotation, innerRadius, outerRadius,
+          innerRoundedness, outerRoundedness);
     }
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/RectangleShape.java b/lottie/src/main/java/com/airbnb/lottie/RectangleShape.java
index 060dc07..04f8dc6 100644
--- a/lottie/src/main/java/com/airbnb/lottie/RectangleShape.java
+++ b/lottie/src/main/java/com/airbnb/lottie/RectangleShape.java
@@ -3,17 +3,25 @@
 import org.json.JSONObject;
 
 class RectangleShape {
-  private static final String TAG = RectangleShape.class.getSimpleName();
-
   private final IAnimatablePathValue position;
   private final AnimatablePointValue size;
   private final AnimatableFloatValue cornerRadius;
 
-  RectangleShape(JSONObject json, LottieComposition composition) {
-    position = AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
-        json.optJSONObject("p"), composition);
-    cornerRadius = new AnimatableFloatValue(json.optJSONObject("r"), composition);
-    size = new AnimatablePointValue(json.optJSONObject("s"), composition);
+  private RectangleShape(IAnimatablePathValue position, AnimatablePointValue size,
+      AnimatableFloatValue cornerRadius) {
+    this.position = position;
+    this.size = size;
+    this.cornerRadius = cornerRadius;
+  }
+
+  static class Factory {
+    static RectangleShape newInstance(JSONObject json, LottieComposition composition) {
+      return new RectangleShape(
+          AnimatablePathValue.createAnimatablePathOrSplitDimensionPath(
+              json.optJSONObject("p"), composition),
+          new AnimatablePointValue(json.optJSONObject("s"), composition),
+          new AnimatableFloatValue(json.optJSONObject("r"), composition));
+    }
   }
 
   AnimatableFloatValue getCornerRadius() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeFill.java b/lottie/src/main/java/com/airbnb/lottie/ShapeFill.java
index ac80362..691a79c 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeFill.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeFill.java
@@ -1,36 +1,52 @@
 package com.airbnb.lottie;
 
+import android.support.annotation.Nullable;
+
 import org.json.JSONObject;
 
 class ShapeFill {
-  private boolean fillEnabled;
-  private AnimatableColorValue color;
-  private AnimatableIntegerValue opacity;
+  private final boolean fillEnabled;
+  @Nullable private final AnimatableColorValue color;
+  @Nullable private final AnimatableIntegerValue opacity;
 
-  ShapeFill(JSONObject json, LottieComposition composition) {
-    JSONObject jsonColor = json.optJSONObject("c");
-    if (jsonColor != null) {
-      color = new AnimatableColorValue(jsonColor, composition);
-    }
-
-    JSONObject jsonOpacity = json.optJSONObject("o");
-    if (jsonOpacity != null) {
-      opacity = new AnimatableIntegerValue(jsonOpacity, composition, false, true);
-    }
-    fillEnabled = json.optBoolean("fillEnabled");
+  private ShapeFill(boolean fillEnabled, @Nullable AnimatableColorValue color, @Nullable
+      AnimatableIntegerValue opacity) {
+    this.fillEnabled = fillEnabled;
+    this.color = color;
+    this.opacity = opacity;
   }
 
-  public AnimatableColorValue getColor() {
+  static class Factory {
+    static ShapeFill newInstance(JSONObject json, LottieComposition composition) {
+      AnimatableColorValue color = null;
+      boolean fillEnabled;
+      AnimatableIntegerValue opacity = null;
+
+      JSONObject jsonColor = json.optJSONObject("c");
+      if (jsonColor != null) {
+        color = new AnimatableColorValue(jsonColor, composition);
+      }
+
+      JSONObject jsonOpacity = json.optJSONObject("o");
+      if (jsonOpacity != null) {
+        opacity = new AnimatableIntegerValue(jsonOpacity, composition, false, true);
+      }
+      fillEnabled = json.optBoolean("fillEnabled");
+      return new ShapeFill(fillEnabled, color, opacity);
+    }
+  }
+
+  @Nullable public AnimatableColorValue getColor() {
     return color;
   }
 
-  public AnimatableIntegerValue getOpacity() {
+  @Nullable public AnimatableIntegerValue getOpacity() {
     return opacity;
   }
 
   @Override
   public String toString() {
-    return "ShapeFill{" + "color=" + Integer.toHexString(color.getInitialValue()) +
+    return "ShapeFill{" + "color=" +  Integer.toHexString(color.getInitialValue()) +
         ", fillEnabled=" + fillEnabled +
         ", opacity=" + opacity.getInitialValue() +
         '}';
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java b/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
index bf13ba9..adc1d73 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeGroup.java
@@ -11,48 +11,55 @@
 import java.util.List;
 
 class ShapeGroup {
-
-  @Nullable
-  static Object shapeItemWithJson(JSONObject json, LottieComposition composition) {
+  @Nullable static Object shapeItemWithJson(JSONObject json, LottieComposition composition) {
     String type = json.optString("ty");
 
     switch (type) {
       case "gr":
-        return new ShapeGroup(json, composition);
+        return ShapeGroup.Factory.newInstance(json, composition);
       case "st":
-        return new ShapeStroke(json, composition);
+        return ShapeStroke.Factory.newInstance(json, composition);
       case "fl":
-        return new ShapeFill(json, composition);
+        return ShapeFill.Factory.newInstance(json, composition);
       case "tr":
-        return new AnimatableTransform(json, composition);
+        return AnimatableTransform.Factory.newInstance(json, composition);
       case "sh":
-        return new ShapePath(json, composition);
+        return ShapePath.Factory.newInstance(json, composition);
       case "el":
-        return new CircleShape(json, composition);
+        return CircleShape.Factory.newInstance(json, composition);
       case "rc":
-        return new RectangleShape(json, composition);
+        return RectangleShape.Factory.newInstance(json, composition);
       case "tm":
-        return new ShapeTrimPath(json, composition);
+        return ShapeTrimPath.Factory.newInstance(json, composition);
       case "sr":
-        return new PolystarShape(json, composition);
+        return PolystarShape.Factory.newInstance(json, composition);
       default:
         Log.w(L.TAG, "Unknown shape type " + type);
     }
     return null;
   }
 
-  private String name;
-  private final List<Object> items = new ArrayList<>();
+  private final String name;
+  private final List<Object> items;
 
-  private ShapeGroup(JSONObject json, LottieComposition composition) {
-    JSONArray jsonItems =  json.optJSONArray("it");
-    name = json.optString("nm");
+  private ShapeGroup(String name, List<Object> items) {
+    this.name = name;
+    this.items = items;
+  }
 
-    for (int i = 0; i < jsonItems.length(); i++) {
-      Object newItem = shapeItemWithJson(jsonItems.optJSONObject(i), composition);
-      if (newItem != null) {
-        items.add(newItem);
+  static class Factory {
+    private static ShapeGroup newInstance(JSONObject json, LottieComposition composition) {
+      JSONArray jsonItems = json.optJSONArray("it");
+      String name = json.optString("nm");
+      List<Object> items = new ArrayList<>();
+
+      for (int i = 0; i < jsonItems.length(); i++) {
+        Object newItem = shapeItemWithJson(jsonItems.optJSONObject(i), composition);
+        if (newItem != null) {
+          items.add(newItem);
+        }
       }
+      return new ShapeGroup(name, items);
     }
   }
 
@@ -60,8 +67,7 @@
     return items;
   }
 
-  @Override
-  public String toString() {
+  @Override public String toString() {
     return "ShapeGroup{" + "name='" + name + "\' Shapes: " + Arrays.toString(items.toArray()) + '}';
   }
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapePath.java b/lottie/src/main/java/com/airbnb/lottie/ShapePath.java
index 7f1a5dc..96672bf 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapePath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapePath.java
@@ -1,20 +1,23 @@
 package com.airbnb.lottie;
 
-import android.util.Log;
-
 import org.json.JSONObject;
 
 class ShapePath {
-  private static final String TAG = ShapePath.class.getSimpleName();
-
   private final String name;
   private final int index;
-  private AnimatableShapeValue shapePath;
+  private final AnimatableShapeValue shapePath;
 
-  ShapePath(JSONObject json, LottieComposition composition) {
-    index = json.optInt("ind");
-    name = json.optString("nm");
-    shapePath = new AnimatableShapeValue(json.optJSONObject("ks"), composition);
+  private ShapePath(String name, int index, AnimatableShapeValue shapePath) {
+    this.name = name;
+    this.index = index;
+    this.shapePath = shapePath;
+  }
+
+  static class Factory {
+    static ShapePath newInstance(JSONObject json, LottieComposition composition) {
+      return new ShapePath(json.optString("nm"), json.optInt("ind"),
+          new AnimatableShapeValue(json.optJSONObject("ks"), composition));
+    }
   }
 
   AnimatableShapeValue getShapePath() {
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeStroke.java b/lottie/src/main/java/com/airbnb/lottie/ShapeStroke.java
index f6ce682..7dfff4f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeStroke.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeStroke.java
@@ -1,5 +1,7 @@
 package com.airbnb.lottie;
 
+import android.support.annotation.Nullable;
+
 import org.json.JSONArray;
 import org.json.JSONObject;
 
@@ -19,42 +21,57 @@
     Bevel
   }
 
-  private AnimatableFloatValue offset;
-  private final List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
-
+  @Nullable private final AnimatableFloatValue offset;
+  private final List<AnimatableFloatValue> lineDashPattern;
   private final AnimatableColorValue color;
   private final AnimatableIntegerValue opacity;
   private final AnimatableFloatValue width;
   private final LineCapType capType;
   private final LineJoinType joinType;
 
-  ShapeStroke(JSONObject json, LottieComposition composition) {
-    color = new AnimatableColorValue(json.optJSONObject("c"), composition);
+  private ShapeStroke(@Nullable AnimatableFloatValue offset,
+      List<AnimatableFloatValue> lineDashPattern, AnimatableColorValue color,
+      AnimatableIntegerValue opacity, AnimatableFloatValue width, LineCapType capType,
+      LineJoinType joinType) {
+    this.offset = offset;
+    this.lineDashPattern = lineDashPattern;
+    this.color = color;
+    this.opacity = opacity;
+    this.width = width;
+    this.capType = capType;
+    this.joinType = joinType;
+  }
 
-    width = new AnimatableFloatValue(json.optJSONObject("w"), composition);
+  static class Factory {
+    static ShapeStroke newInstance(JSONObject json, LottieComposition composition) {
+      List<AnimatableFloatValue> lineDashPattern = new ArrayList<>();
+      AnimatableColorValue color = new AnimatableColorValue(json.optJSONObject("c"), composition);
+      AnimatableFloatValue width = new AnimatableFloatValue(json.optJSONObject("w"), composition);
+      AnimatableIntegerValue opacity = new AnimatableIntegerValue(json.optJSONObject("o"),
+          composition, false, true);
+      LineCapType capType = LineCapType.values()[json.optInt("lc") - 1];
+      LineJoinType joinType = LineJoinType.values()[json.optInt("lj") - 1];
+      AnimatableFloatValue offset = null;
 
-    opacity = new AnimatableIntegerValue(json.optJSONObject("o"), composition, false, true);
-
-    capType = LineCapType.values()[json.optInt("lc") - 1];
-    joinType = LineJoinType.values()[json.optInt("lj") - 1];
-
-    if (json.has("d")) {
-      JSONArray dashesJson = json.optJSONArray("d");
-      for (int i = 0; i < dashesJson.length(); i++) {
-        JSONObject dashJson = dashesJson.optJSONObject(i);
-        String n = dashJson.optString("n");
-        if (n.equals("o")) {
-          JSONObject value = dashJson.optJSONObject("v");
-          offset = new AnimatableFloatValue(value, composition);
-        } else if (n.equals("d") || n.equals("g")) {
-          JSONObject value = dashJson.optJSONObject("v");
-          lineDashPattern.add(new AnimatableFloatValue(value, composition));
+      if (json.has("d")) {
+        JSONArray dashesJson = json.optJSONArray("d");
+        for (int i = 0; i < dashesJson.length(); i++) {
+          JSONObject dashJson = dashesJson.optJSONObject(i);
+          String n = dashJson.optString("n");
+          if (n.equals("o")) {
+            JSONObject value = dashJson.optJSONObject("v");
+            offset = new AnimatableFloatValue(value, composition);
+          } else if (n.equals("d") || n.equals("g")) {
+            JSONObject value = dashJson.optJSONObject("v");
+            lineDashPattern.add(new AnimatableFloatValue(value, composition));
+          }
+        }
+        if (lineDashPattern.size() == 1) {
+          // If there is only 1 value then it is assumed to be equal parts on and off.
+          lineDashPattern.add(lineDashPattern.get(0));
         }
       }
-      if (lineDashPattern.size() == 1) {
-        // If there is only 1 value then it is assumed to be equal parts on and off.
-        lineDashPattern.add(lineDashPattern.get(0));
-      }
+      return new ShapeStroke(offset, lineDashPattern, color, opacity, width, capType, joinType);
     }
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
index 19128e8..570684f 100644
--- a/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
+++ b/lottie/src/main/java/com/airbnb/lottie/ShapeTrimPath.java
@@ -7,10 +7,20 @@
   private final AnimatableFloatValue end;
   private final AnimatableFloatValue offset;
 
-  ShapeTrimPath(JSONObject json, LottieComposition composition) {
-    start = new AnimatableFloatValue(json.optJSONObject("s"), composition, false);
-    end = new AnimatableFloatValue(json.optJSONObject("e"), composition, false);
-    offset = new AnimatableFloatValue(json.optJSONObject("o"), composition, false);
+  private ShapeTrimPath(AnimatableFloatValue start, AnimatableFloatValue end, AnimatableFloatValue
+      offset) {
+    this.start = start;
+    this.end = end;
+    this.offset = offset;
+  }
+
+  static class Factory {
+    static ShapeTrimPath newInstance(JSONObject json, LottieComposition composition) {
+      return new ShapeTrimPath(
+          new AnimatableFloatValue(json.optJSONObject("s"), composition, false),
+          new AnimatableFloatValue(json.optJSONObject("e"), composition, false),
+          new AnimatableFloatValue(json.optJSONObject("o"), composition, false));
+    }
   }
 
   AnimatableFloatValue getEnd() {