Optimize fill
diff --git a/lottie/src/main/java/com/airbnb/lottie/L.java b/lottie/src/main/java/com/airbnb/lottie/L.java
index a27d7da..68f7ca1 100644
--- a/lottie/src/main/java/com/airbnb/lottie/L.java
+++ b/lottie/src/main/java/com/airbnb/lottie/L.java
@@ -5,7 +5,6 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
-import androidx.core.os.TraceCompat;
 
 import com.airbnb.lottie.network.DefaultLottieNetworkFetcher;
 import com.airbnb.lottie.network.LottieNetworkCacheProvider;
@@ -15,6 +14,8 @@
 import com.airbnb.lottie.utils.LottieTrace;
 
 import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 public class L {
@@ -33,6 +34,8 @@
   private static volatile NetworkCache networkCache;
   private static ThreadLocal<LottieTrace> lottieTrace;
 
+  public static AtomicLong drawTimeNs = new AtomicLong(0L);
+
   private L() {
   }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index e493288..2d39708 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -680,7 +680,10 @@
         renderAndDrawAsBitmap(canvas, compositionLayer);
         canvas.restore();
       } else {
+        long start = System.nanoTime();
         compositionLayer.draw(canvas, matrix, alpha);
+        long end = System.nanoTime();
+        L.drawTimeNs.getAndAdd(end - start);
       }
       isDirty = false;
     } catch (InterruptedException e) {
@@ -1560,6 +1563,7 @@
       return;
     }
 
+    long start = System.nanoTime();
     renderingMatrix.reset();
     Rect bounds = getBounds();
     if (!bounds.isEmpty()) {
@@ -1571,6 +1575,8 @@
       renderingMatrix.preTranslate(bounds.left, bounds.top);
     }
     compositionLayer.draw(canvas, renderingMatrix, alpha);
+    long end = System.nanoTime();
+    L.drawTimeNs.getAndAdd(end - start);
   }
 
   /**
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/ContentGroup.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/ContentGroup.java
index cc3d166..2fc907a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/ContentGroup.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/ContentGroup.java
@@ -30,6 +30,7 @@
 
   private final Paint offScreenPaint = new LPaint();
   private final RectF offScreenRectF = new RectF();
+  private int generation;
 
   private static List<Content> contentsFromModels(LottieDrawable drawable, LottieComposition composition, BaseLayer layer,
       List<ContentModel> contentModels) {
@@ -160,6 +161,17 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    int result = 0;
+    for (int i = contents.size() - 1; i >= 0; i--) {
+      Content content = contents.get(i);
+      if (content instanceof PathContent) {
+        result = 31 * result + ((PathContent) content).getGeneration();
+      }
+    }
+    return result;
+  }
+
   @Override public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
     if (hidden) {
       return;
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/EllipseContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/EllipseContent.java
index 031d8d5..7e49f01 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/EllipseContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/EllipseContent.java
@@ -31,6 +31,7 @@
 
   private final CompoundTrimPathContent trimPaths = new CompoundTrimPathContent();
   private boolean isPathValid;
+  private int generation = 0;
 
   public EllipseContent(LottieDrawable lottieDrawable, BaseLayer layer, CircleShape circleShape) {
     name = circleShape.getName();
@@ -52,6 +53,7 @@
 
   private void invalidate() {
     isPathValid = false;
+    generation++;
     lottieDrawable.invalidateSelf();
   }
 
@@ -116,6 +118,10 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    return generation;
+  }
+
   @Override public void resolveKeyPath(
       KeyPath keyPath, int depth, List<KeyPath> accumulator, KeyPath currentPartialKeyPath) {
     MiscUtils.resolveKeyPath(keyPath, depth, accumulator, currentPartialKeyPath, this);
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
index 46732a8..cec25b7 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/FillContent.java
@@ -4,6 +4,7 @@
 
 import android.graphics.BlurMaskFilter;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Paint;
@@ -32,6 +33,8 @@
 public class FillContent
     implements DrawingContent, BaseKeyframeAnimation.AnimationListener, KeyPathElementContent {
   private final Path path = new Path();
+  private final Matrix pathWithParentMatrixMatrix = new Matrix();
+  private int pathGeneration = -23094820;
   private final Paint paint = new LPaint(Paint.ANTI_ALIAS_FLAG);
   private final BaseLayer layer;
   private final String name;
@@ -44,6 +47,12 @@
   @Nullable private BaseKeyframeAnimation<Float, Float> blurAnimation;
   float blurMaskFilterRadius;
 
+  private static final int DIRTY_FLAG_COLOR = 1;
+  private static final int DIRTY_FLAG_OPACITY = 1 << 1;
+  private int colorDirtyFlags = DIRTY_FLAG_COLOR | DIRTY_FLAG_OPACITY;
+  private int colorFromAnimations = 0;
+  private int lastAlpha = Integer.MAX_VALUE;
+
   @Nullable private DropShadowKeyframeAnimation dropShadowAnimation;
 
   public FillContent(final LottieDrawable lottieDrawable, BaseLayer layer, ShapeFill fill) {
@@ -69,10 +78,16 @@
     path.setFillType(fill.getFillType());
 
     colorAnimation = fill.getColor().createAnimation();
-    colorAnimation.addUpdateListener(this);
+    colorAnimation.addUpdateListener(() -> {
+      colorDirtyFlags |= DIRTY_FLAG_COLOR;
+      onValueChanged();
+    });
     layer.addAnimation(colorAnimation);
     opacityAnimation = fill.getOpacity().createAnimation();
-    opacityAnimation.addUpdateListener(this);
+    opacityAnimation.addUpdateListener(() -> {
+      colorDirtyFlags |= DIRTY_FLAG_OPACITY;
+      onValueChanged();
+    });
     layer.addAnimation(opacityAnimation);
   }
 
@@ -98,9 +113,23 @@
       return;
     }
     L.beginSection("FillContent#draw");
-    int color = ((ColorKeyframeAnimation) this.colorAnimation).getIntValue();
-    int alpha = (int) ((parentAlpha / 255f * opacityAnimation.getValue() / 100f) * 255);
-    paint.setColor((clamp(alpha, 0, 255) << 24) | (color & 0xFFFFFF));
+
+    boolean hasDirtyFlags = colorDirtyFlags != 0;
+    if ((colorDirtyFlags & DIRTY_FLAG_COLOR) != 0) {
+      int color = ((ColorKeyframeAnimation) this.colorAnimation).getIntValue();
+      colorFromAnimations = (colorFromAnimations & 0xFF000000) | (color & 0xFFFFFF);
+      colorDirtyFlags &= ~DIRTY_FLAG_COLOR;
+    }
+    if ((colorDirtyFlags & DIRTY_FLAG_OPACITY) != 0) {
+      int alpha = (int) ((opacityAnimation.getValue() / 100f) * 255);
+      colorFromAnimations = (clamp(alpha, 0, 255) << 24) | (colorFromAnimations & 0xFFFFFF);
+      colorDirtyFlags &= ~DIRTY_FLAG_OPACITY;
+    }
+    int finalAlpha = Color.alpha(colorFromAnimations) * parentAlpha / 255;
+    if (finalAlpha != lastAlpha || hasDirtyFlags) {
+      paint.setColor(finalAlpha << 24 | (colorFromAnimations & 0xFFFFFF));
+    }
+    lastAlpha = finalAlpha;
 
     if (colorFilterAnimation != null) {
       paint.setColorFilter(colorFilterAnimation.getValue());
@@ -120,16 +149,34 @@
       dropShadowAnimation.applyTo(paint);
     }
 
-    path.reset();
-    for (int i = 0; i < paths.size(); i++) {
-      path.addPath(paths.get(i).getPath(), parentMatrix);
-    }
-
+    updatePath(parentMatrix);
     canvas.drawPath(path, paint);
-
     L.endSection("FillContent#draw");
   }
 
+  private void updatePath(Matrix parentMatrix) {
+    int pathGeneration = pathGeneration();
+    if (this.pathGeneration != pathGeneration || !parentMatrix.equals(pathWithParentMatrixMatrix)) {
+      path.rewind();
+      for (int i = 0; i < paths.size(); i++) {
+        Path newPath = paths.get(i).getPath();
+        path.addPath(newPath);
+      }
+      path.transform(parentMatrix);
+      this.pathGeneration = pathGeneration;
+      pathWithParentMatrixMatrix.set(parentMatrix);
+    }
+  }
+
+  private int pathGeneration() {
+    int result = 0;
+    for (int i = paths.size() - 1; i >= 0; i--) {
+      PathContent content = paths.get(i);
+      result = 31 * result + content.getGeneration();
+    }
+    return result;
+  }
+
   @Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) {
     path.reset();
     for (int i = 0; i < paths.size(); i++) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/MergePathsContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/MergePathsContent.java
index b69d713..5ad5388 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/MergePathsContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/MergePathsContent.java
@@ -76,6 +76,15 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    int result = 0;
+    for (int i = pathContents.size() - 1; i >= 0; i--) {
+      PathContent content = pathContents.get(i);
+      result = 31 * result + content.getGeneration();
+    }
+    return result;
+  }
+
   @Override public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/PathContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/PathContent.java
index c7f6bfc..5e4736e 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/PathContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/PathContent.java
@@ -4,4 +4,5 @@
 
 interface PathContent extends Content {
   Path getPath();
+  int getGeneration();
 }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/PolystarContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/PolystarContent.java
index cfa9523..ec05d44 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/PolystarContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/PolystarContent.java
@@ -44,6 +44,7 @@
 
   private final CompoundTrimPathContent trimPaths = new CompoundTrimPathContent();
   private boolean isPathValid;
+  private int generation;
 
   public PolystarContent(LottieDrawable lottieDrawable, BaseLayer layer,
       PolystarShape polystarShape) {
@@ -93,6 +94,7 @@
 
   private void invalidate() {
     isPathValid = false;
+    generation++;
     lottieDrawable.invalidateSelf();
   }
 
@@ -137,6 +139,10 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    return generation;
+  }
+
   @Override public String getName() {
     return name;
   }
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
index 89a966f..a883397 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/RectangleContent.java
@@ -35,6 +35,7 @@
   /** This corner radius is from a layer item. The first one is from the roundedness on this specific rect. */
   @Nullable private BaseKeyframeAnimation<Float, Float> roundedCornersAnimation = null;
   private boolean isPathValid;
+  private int generation = 0;
 
   public RectangleContent(LottieDrawable lottieDrawable, BaseLayer layer, RectangleShape rectShape) {
     name = rectShape.getName();
@@ -65,6 +66,7 @@
 
   private void invalidate() {
     isPathValid = false;
+    generation++;
     lottieDrawable.invalidateSelf();
   }
 
@@ -161,6 +163,10 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    return generation;
+  }
+
   @Override
   public void resolveKeyPath(KeyPath keyPath, int depth, List<KeyPath> accumulator,
       KeyPath currentPartialKeyPath) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/RepeaterContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/RepeaterContent.java
index 4a721fb..82a57b9 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/RepeaterContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/RepeaterContent.java
@@ -105,6 +105,11 @@
     return path;
   }
 
+  @Override
+  public int getGeneration() {
+    return contentGroup.getGeneration();
+  }
+
   @Override public void draw(Canvas canvas, Matrix parentMatrix, int alpha) {
     float copies = this.copies.getValue();
     float offset = this.offset.getValue();
diff --git a/lottie/src/main/java/com/airbnb/lottie/animation/content/ShapeContent.java b/lottie/src/main/java/com/airbnb/lottie/animation/content/ShapeContent.java
index 1f54ac0..7b679cd 100644
--- a/lottie/src/main/java/com/airbnb/lottie/animation/content/ShapeContent.java
+++ b/lottie/src/main/java/com/airbnb/lottie/animation/content/ShapeContent.java
@@ -24,6 +24,7 @@
   @Nullable private List<ShapeModifierContent> shapeModifierContents;
 
   private boolean isPathValid;
+  private int generation = 0;
   private final CompoundTrimPathContent trimPaths = new CompoundTrimPathContent();
 
   public ShapeContent(LottieDrawable lottieDrawable, BaseLayer layer, ShapePath shape) {
@@ -41,6 +42,7 @@
 
   private void invalidate() {
     isPathValid = false;
+    generation++;
     lottieDrawable.invalidateSelf();
   }
 
@@ -91,6 +93,10 @@
     return path;
   }
 
+  @Override public int getGeneration() {
+    return generation;
+  }
+
   @Override public String getName() {
     return name;
   }