Add an option to not clip animations to the composition bounds
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
index 679a6ef..b06f5cd 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieAnimation.kt
@@ -69,6 +69,7 @@
     dynamicProperties: LottieDynamicProperties? = null,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
+    clipToBounds: Boolean = true,
 ) {
     val drawable = remember { LottieDrawable() }
     val matrix = remember { Matrix() }
@@ -101,6 +102,7 @@
             drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
             drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
             drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
+            drawable.clipToCompositionBounds = clipToBounds
             drawable.progress = progress
             drawable.draw(canvas.nativeCanvas, matrix)
         }
@@ -129,7 +131,8 @@
     dynamicProperties: LottieDynamicProperties? = null,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
-) {
+    clipToBounds: Boolean = true,
+    ) {
     val progress by animateLottieCompositionAsState(
         composition,
         isPlaying,
@@ -148,6 +151,7 @@
         dynamicProperties,
         alignment,
         contentScale,
+        clipToBounds,
     )
 }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index a34478f..0cb8f69 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -228,6 +228,13 @@
         )
     );
 
+    lottieDrawable.setClipToCompositionBounds(
+        ta.getBoolean(
+            R.styleable.LottieAnimationView_lottie_clipToCompositionBounds,
+            true
+        )
+    );
+
     ta.recycle();
 
     lottieDrawable.setSystemAnimationsAreEnabled(Utils.getAnimationScale(getContext()) != 0f);
@@ -1072,6 +1079,22 @@
   }
 
   /**
+   * Set this to false to prevent Lottie from clipping the animation rendering to the root composition bounds.
+   * This only affects the root composition. Nested compositions (precomposing) will still be clipped.
+   * Defaults to true.
+   */
+  public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
+    lottieDrawable.setClipToCompositionBounds(clipToCompositionBounds);
+  }
+
+  /**
+   * Returns whether or not the rendering will be clipped to the root composition bounds.
+   */
+  public boolean getClipToCompositionBounds() {
+    return lottieDrawable.getClipToCompositionBounds();
+  }
+
+  /**
    * If rendering via software, Android will fail to generate a bitmap if the view is too large. Rather than displaying
    * nothing, fallback on hardware acceleration which may incur a performance hit.
    *
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index b235b39..62f2c18 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -67,6 +67,7 @@
   private boolean ignoreSystemAnimationsDisabled = false;
 
   private boolean safeMode = false;
+  private boolean clipToCompositionBounds = true;
 
   private final ArrayList<LazyCompositionTask> lazyCompositionTasks = new ArrayList<>();
   private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@@ -344,6 +345,25 @@
     this.safeMode = safeMode;
   }
 
+  /**
+   * Set this to false to prevent Lottie from clipping the animation rendering to the root composition bounds.
+   * This only affects the root composition. Nested compositions (precomposing) will still be clipped.
+   * Defaults to true.
+   */
+  public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
+    if (clipToCompositionBounds != this.clipToCompositionBounds) {
+      this.clipToCompositionBounds = clipToCompositionBounds;
+      invalidateSelf();
+    }
+  }
+
+  /**
+   * Returns whether or not the rendering will be clipped to the root composition bounds.
+   */
+  public boolean getClipToCompositionBounds() {
+    return clipToCompositionBounds;
+  }
+
   @Override
   public void invalidateSelf() {
     if (isDirty) {
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
index c20ae75..7f24db7 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/CompositionLayer.java
@@ -112,7 +112,7 @@
     int childAlpha = isDrawingWithOffScreen ? 255 : parentAlpha;
     for (int i = layers.size() - 1; i >= 0; i--) {
       boolean nonEmptyClip = true;
-      if (!newClipRect.isEmpty()) {
+      if (!newClipRect.isEmpty() && !("__container".equals(getName()) && !lottieDrawable.getClipToCompositionBounds())) {
         nonEmptyClip = canvas.clipRect(newClipRect);
       }
       if (nonEmptyClip) {
diff --git a/lottie/src/main/res/values/attrs.xml b/lottie/src/main/res/values/attrs.xml
index 1e8f30e..c0aa02f 100644
--- a/lottie/src/main/res/values/attrs.xml
+++ b/lottie/src/main/res/values/attrs.xml
@@ -21,6 +21,7 @@
         <attr name="lottie_scale" format="float" />
         <attr name="lottie_speed" format="float" />
         <attr name="lottie_cacheComposition" format="boolean" />
+        <attr name="lottie_clipToCompositionBounds" format="boolean" />
         <attr name="lottie_ignoreDisabledSystemAnimations" format="boolean" />
         <!-- These values must be kept in sync with the RenderMode enum -->
         <attr name="lottie_renderMode" format="enum">
diff --git a/sample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt b/sample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
index af2c9fc..da5de5c 100644
--- a/sample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
+++ b/sample/src/androidTest/java/com/airbnb/lottie/samples/LottieTest.kt
@@ -115,6 +115,7 @@
             testNightMode()
             testApplyOpacityToLayer()
             testOutlineMasksAndMattes()
+            testDontClipCompositionBounds()
             snapshotter.finalizeReportAndUpload()
         }
     }
@@ -1077,6 +1078,16 @@
         }
     }
 
+    private suspend fun testDontClipCompositionBounds() {
+        withFilmStripView(
+            "Tests/ClipComposition.json",
+            "Clip Composition Bounds",
+            "False"
+        ) { filmStripView ->
+            filmStripView.setClipToCompositionBounds(true)
+        }
+    }
+
     private suspend fun testCustomBounds() {
         val composition = LottieCompositionFactory.fromRawResSync(application, R.raw.heart).value!!
         val bitmap = bitmapPool.acquire(50, 100)
diff --git a/sample/src/main/assets/Tests/ClipComposition.json b/sample/src/main/assets/Tests/ClipComposition.json
new file mode 100644
index 0000000..1ad68b1
--- /dev/null
+++ b/sample/src/main/assets/Tests/ClipComposition.json
@@ -0,0 +1 @@
+{"v":"5.7.13","fr":29.9700012207031,"ip":0,"op":178.000007250089,"w":300,"h":300,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,150,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[174.341,174.341],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-126.83,-100.83],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":178.000007250089,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/sample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt b/sample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
index d93961b..3ce56e4 100644
--- a/sample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
+++ b/sample/src/main/kotlin/com/airbnb/lottie/samples/views/FilmStripView.kt
@@ -44,4 +44,8 @@
     fun setOutlineMasksAndMattes(outline: Boolean) {
         animationViews.forEach { it.setOutlineMasksAndMattes(outline) }
     }
+
+    fun setClipToCompositionBounds(clip: Boolean) {
+        animationViews.forEach { it.clipToCompositionBounds = clip }
+    }
 }