Allow Lottie to render the full animation, even if it extends beyond the original composition bounds (#1993)

Since the beginning of time, Lottie has only rendered the bounds of the original composition. This PR adds a new API to enable rendering the full animation, even if it extends beyond the original composition bounds.

This API defaults to off to retain backwards compatibility.

Fixes #1825
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 886747c..ed4b10c 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
@@ -59,6 +59,7 @@
  * @param alignment Define where the animation should be placed within this composable if it has a different
  *                  size than this composable.
  * @param contentScale Define how the animation should be scaled if it has a different size than this Composable.
+ * @param clipToComposition Determines whether or not Lottie will clip the animation to the original animation composition bounds.
  */
 @Composable
 fun LottieAnimation(
@@ -72,6 +73,7 @@
     dynamicProperties: LottieDynamicProperties? = null,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
+    clipToComposition: Boolean = true,
 ) {
     val drawable = remember { LottieDrawable() }
     val matrix = remember { Matrix() }
@@ -106,6 +108,7 @@
             drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
             drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
             drawable.useSoftwareRendering(useSoftwareRendering)
+            drawable.clipToCompositionBounds = clipToComposition
             drawable.progress = progress
             drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
             drawable.draw(canvas.nativeCanvas, matrix)
@@ -136,6 +139,7 @@
     dynamicProperties: LottieDynamicProperties? = null,
     alignment: Alignment = Alignment.Center,
     contentScale: ContentScale = ContentScale.Fit,
+    clipToComposition: Boolean = true,
 ) {
     val progress by animateLottieCompositionAsState(
         composition,
@@ -156,6 +160,7 @@
         dynamicProperties,
         alignment,
         contentScale,
+        clipToComposition,
     )
 }
 
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 908e2e5..bd8e560 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -184,6 +184,10 @@
       setSpeed(ta.getFloat(R.styleable.LottieAnimationView_lottie_speed, 1f));
     }
 
+    if (ta.hasValue(R.styleable.LottieAnimationView_lottie_clipToCompositionBounds)) {
+      setClipToCompositionBounds(ta.getBoolean(R.styleable.LottieAnimationView_lottie_clipToCompositionBounds, true));
+    }
+
     setImageAssetsFolder(ta.getString(R.styleable.LottieAnimationView_lottie_imageAssetsFolder));
     setProgress(ta.getFloat(R.styleable.LottieAnimationView_lottie_progress, 0));
     enableMergePathsForKitKatAndAbove(ta.getBoolean(
@@ -333,6 +337,26 @@
   }
 
   /**
+   * Sets whether or not Lottie should clip to the original animation composition bounds.
+   *
+   * When set to true, the parent view may need to disable clipChildren so Lottie can render outside of the LottieAnimationView bounds.
+   *
+   * Defaults to true.
+   */
+  public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
+    lottieDrawable.setClipToCompositionBounds(clipToCompositionBounds);
+  }
+
+  /**
+   * Gets whether or not Lottie should clip to the original animation composition bounds.
+   *
+   * Defaults to true.
+   */
+  public boolean getClipToCompositionBounds() {
+    return lottieDrawable.getClipToCompositionBounds();
+  }
+
+  /**
    * If set to true, all future compositions that are set will be cached so that they don't need to be parsed
    * next time they are loaded. This won't apply to compositions that have already been loaded.
    * <p>
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index ee9602f..d801f80 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -112,6 +112,7 @@
   @Nullable
   TextDelegate textDelegate;
   private boolean enableMergePaths;
+  private boolean clipToCompositionBounds = true;
   @Nullable
   private CompositionLayer compositionLayer;
   private int alpha = 255;
@@ -209,6 +210,27 @@
   }
 
   /**
+   * Sets whether or not Lottie should clip to the original animation composition bounds.
+   *
+   * Defaults to true.
+   */
+  public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
+    if (clipToCompositionBounds != this.clipToCompositionBounds) {
+      this.clipToCompositionBounds = clipToCompositionBounds;
+      invalidateSelf();
+    }
+  }
+
+  /**
+   * Gets whether or not Lottie should clip to the original animation composition bounds.
+   *
+   * Defaults to true.
+   */
+  public boolean getClipToCompositionBounds() {
+    return clipToCompositionBounds;
+  }
+
+  /**
    * If you use image assets, you must explicitly specify the folder in assets/ in which they are
    * located because bodymovin uses the name filenames across all compositions (img_#).
    * Do NOT rename the images themselves.
@@ -434,7 +456,6 @@
     } else {
       drawInternal(canvas);
     }
-    isDirty = false;
 
     L.endSection("Drawable#draw");
   }
@@ -445,6 +466,7 @@
     } else {
       drawWithOriginalAspectRatio(canvas);
     }
+    isDirty = false;
   }
 
   private boolean boundsMatchesCompositionAspectRatio() {
@@ -860,6 +882,7 @@
   }
 
 
+  @SuppressWarnings("unused")
   public boolean isLooping() {
     return animator.getRepeatCount() == ValueAnimator.INFINITE;
   }
@@ -1221,7 +1244,10 @@
     }
 
     if (softwareRenderingEnabled) {
-      renderAndDrawAsBitmap(canvas, compositionLayer, matrix);
+      canvas.save();
+      canvas.concat(matrix);
+      renderAndDrawAsBitmap(canvas, compositionLayer);
+      canvas.restore();
     } else {
       compositionLayer.draw(canvas, matrix, alpha);
     }
@@ -1235,7 +1261,7 @@
     }
 
     if (softwareRenderingEnabled) {
-      renderAndDrawAsBitmap(canvas, compositionLayer, null);
+      renderAndDrawAsBitmap(canvas, compositionLayer);
     } else {
       Rect bounds = getBounds();
       // In fitXY mode, the scale doesn't take effect.
@@ -1257,7 +1283,7 @@
     }
 
     if (softwareRenderingEnabled) {
-      renderAndDrawAsBitmap(canvas, compositionLayer, null);
+      renderAndDrawAsBitmap(canvas, compositionLayer);
     } else {
       renderingMatrix.reset();
       renderingMatrix.preScale(scale, scale);
@@ -1272,23 +1298,38 @@
    * @see LottieDrawable#useSoftwareRendering(boolean)
    * @see LottieAnimationView#setRenderMode(RenderMode)
    */
-  private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer, @Nullable Matrix parentMatrix) {
+  private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer) {
     ensureSoftwareRenderingObjectsInitialized();
 
     //noinspection deprecation
     originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
     softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
     renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
-    if (parentMatrix != null) {
-      renderingMatrix.postConcat(parentMatrix);
-    }
 
-    // Determine what bounds the animation will render to after taking into account the canvas and parent matrix.
-    softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
+    // The bounds are usually intrinsicWidth x intrinsicHeight. If they are different, an external source is scaling this drawable.
+    // This is how ImageView.ScaleType.FIT_XY works.
+    Rect bounds = getBounds();
+    float scaleX = bounds.width() / (float) getIntrinsicWidth();
+    float scaleY = bounds.height() / (float) getIntrinsicHeight();
+
+    if (clipToCompositionBounds) {
+      // Only render the intrinsic (composition) bounds.
+      softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
+    } else {
+      // Find the full bounds of the animation.
+      softwareRenderingTransformedBounds.set(0f, 0f, 0f, 0f);
+      compositionLayer.getBounds(softwareRenderingTransformedBounds, null, false);
+    }
+    softwareRenderingTransformedBounds.set(
+        softwareRenderingTransformedBounds.left * scaleX,
+        softwareRenderingTransformedBounds.top * scaleY,
+        softwareRenderingTransformedBounds.right * scaleX,
+        softwareRenderingTransformedBounds.bottom * scaleY
+    );
+
+    // Transform the animation bounds to the bounds that they will render to on the canvas.
     renderingMatrix.mapRect(softwareRenderingTransformedBounds);
 
-    // We only need to render the portion of the animation that intersects with the canvas's bounds.
-    softwareRenderingTransformedBounds.intersect(0f, 0f, originalCanvas.getWidth(), originalCanvas.getHeight());
 
     int renderWidth = (int) Math.ceil(softwareRenderingTransformedBounds.width());
     int renderHeight = (int) Math.ceil(softwareRenderingTransformedBounds.height());
@@ -1303,10 +1344,7 @@
 
     if (isDirty) {
       softwareRenderingBitmap.eraseColor(0);
-      renderingMatrix.preScale(scale, scale);
-      // The bounds are usually intrinsicWidth x intrinsicHeight. If they are different, an external source is scaling this drawable.
-      // This is how ImageView.ScaleType.FIT_XY works.
-      renderingMatrix.preScale(getBounds().width() / (float) getIntrinsicWidth(), getBounds().height() / (float) getIntrinsicHeight());
+      renderingMatrix.preScale(scale * scaleX, scale * scaleY);
       // We want to render the smallest bitmap possible. If the animation doesn't start at the top left, we translate the canvas and shrink the
       // bitmap to avoid allocating and copying the empty space on the left and top. renderWidth and renderHeight take this into account.
       renderingMatrix.postTranslate(-softwareRenderingTransformedBounds.left, -softwareRenderingTransformedBounds.top);
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..43ea6a1 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 (lottieDrawable.getClipToCompositionBounds() && !newClipRect.isEmpty()) {
         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..73e3481 100644
--- a/lottie/src/main/res/values/attrs.xml
+++ b/lottie/src/main/res/values/attrs.xml
@@ -28,5 +28,6 @@
             <enum name="hardware" value="1" />
             <enum name="software" value="2" />
         </attr>
+        <attr name="lottie_clipToCompositionBounds" format="boolean" />
     </declare-styleable>
 </resources>
\ No newline at end of file
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
index e758e18..fec2593 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/LottieSnapshotTest.kt
@@ -17,6 +17,7 @@
 import com.airbnb.lottie.snapshots.tests.AssetsTestCase
 import com.airbnb.lottie.snapshots.tests.ColorStateListColorFilterTestCase
 import com.airbnb.lottie.snapshots.tests.ComposeDynamicPropertiesTestCase
+import com.airbnb.lottie.snapshots.tests.ComposeScaleTypesTestCase
 import com.airbnb.lottie.snapshots.tests.CustomBoundsTestCase
 import com.airbnb.lottie.snapshots.tests.DynamicPropertiesTestCase
 import com.airbnb.lottie.snapshots.tests.FailureTestCase
@@ -110,6 +111,7 @@
             FailureTestCase(),
             FrameBoundariesTestCase(),
             ScaleTypesTestCase(),
+            ComposeScaleTypesTestCase(),
             DynamicPropertiesTestCase(),
             MarkersTestCase(),
             AssetsTestCase(),
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/SnapshotTestCaseContext.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/SnapshotTestCaseContext.kt
index f254de1..b7e4e4b 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/SnapshotTestCaseContext.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/SnapshotTestCaseContext.kt
@@ -14,20 +14,18 @@
 import android.widget.LinearLayout
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.platform.ComposeView
-import androidx.core.view.doOnAttach
 import androidx.core.view.doOnLayout
-import androidx.core.view.doOnPreDraw
 import com.airbnb.lottie.FontAssetDelegate
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieComposition
 import com.airbnb.lottie.LottieCompositionFactory
 import com.airbnb.lottie.LottieDrawable
+import com.airbnb.lottie.RenderMode
 import com.airbnb.lottie.model.LottieCompositionCache
 import com.airbnb.lottie.snapshots.utils.BitmapPool
 import com.airbnb.lottie.snapshots.utils.HappoSnapshotter
 import com.airbnb.lottie.snapshots.utils.ObjectPool
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.android.awaitFrame
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.withContext
@@ -78,6 +76,7 @@
     snapshotVariant: String = "default",
     widthPx: Int = context.resources.displayMetrics.widthPixels,
     heightPx: Int = context.resources.displayMetrics.heightPixels,
+    renderHardwareAndSoftware: Boolean = false,
     callback: (LottieAnimationView) -> Unit,
 ) {
     val result = LottieCompositionFactory.fromAssetSync(context, assetName)
@@ -101,10 +100,25 @@
     animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
     val bitmap = bitmapPool.acquire(animationView.width, animationView.height)
     val canvas = Canvas(bitmap)
-    log("Drawing $assetName")
-    animationView.draw(canvas)
-    animationViewPool.release(animationView)
-    snapshotter.record(bitmap, snapshotName, snapshotVariant)
+    if (renderHardwareAndSoftware) {
+        log("Drawing $assetName - hardware")
+        val renderMode = animationView.renderMode
+        animationView.renderMode = RenderMode.HARDWARE
+        animationView.draw(canvas)
+        snapshotter.record(bitmap, snapshotName, "$snapshotVariant - Hardware")
+
+        bitmap.eraseColor(0)
+        animationView.renderMode = RenderMode.SOFTWARE
+        animationView.draw(canvas)
+        animationViewPool.release(animationView)
+        snapshotter.record(bitmap, snapshotName, "$snapshotVariant - Software")
+        animationView.renderMode = renderMode
+    } else {
+        log("Drawing $assetName")
+        animationView.draw(canvas)
+        animationViewPool.release(animationView)
+        snapshotter.record(bitmap, snapshotName, snapshotVariant)
+    }
     bitmapPool.release(bitmap)
 }
 
@@ -153,23 +167,30 @@
     bitmapPool.release(bitmap)
 }
 
+fun SnapshotTestCaseContext.loadCompositionFromAssetsSync(fileName: String): LottieComposition {
+    return LottieCompositionFactory.fromAssetSync(context, fileName).value!!
+}
+
 suspend fun SnapshotTestCaseContext.snapshotComposable(
     name: String,
     variant: String = "default",
-    content: @Composable () -> Unit,
+    renderHardwareAndSoftware: Boolean = false,
+    content: @Composable (RenderMode) -> Unit,
 ) = withContext(Dispatchers.Default) {
     log("Snapshotting $name")
     val composeView = ComposeView(context)
     composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
-    val bitmap = withContext(Dispatchers.Main) {
-        composeView.setContent(content)
+    var bitmap = withContext(Dispatchers.Main) {
+        composeView.setContent {
+            content(RenderMode.SOFTWARE)
+        }
         suspendCancellableCoroutine<Bitmap> { cont ->
             composeView.doOnLayout {
                 log("Drawing $name")
-                val bitmap = bitmapPool.acquire(composeView.width, composeView.height)
-                val canvas = Canvas(bitmap)
+                val b = bitmapPool.acquire(composeView.width, composeView.height)
+                val canvas = Canvas(b)
                 composeView.draw(canvas)
-                cont.resume(bitmap)
+                cont.resume(b)
             }
             onActivity { activity ->
                 activity.binding.content.addView(composeView)
@@ -179,7 +200,33 @@
     onActivity { activity ->
         activity.binding.content.removeView(composeView)
     }
-    LottieCompositionCache.getInstance().clear()
-    snapshotter.record(bitmap, name, variant)
+    snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Software" else variant)
     bitmapPool.release(bitmap)
+
+    if (renderHardwareAndSoftware) {
+        bitmap = withContext(Dispatchers.Main) {
+            composeView.setContent {
+                content(RenderMode.HARDWARE)
+            }
+            suspendCancellableCoroutine { cont ->
+                composeView.doOnLayout {
+                    log("Drawing $name")
+                    val b = bitmapPool.acquire(composeView.width, composeView.height)
+                    val canvas = Canvas(b)
+                    composeView.draw(canvas)
+                    cont.resume(b)
+                }
+                onActivity { activity ->
+                    activity.binding.content.addView(composeView)
+                }
+            }
+        }
+        onActivity { activity ->
+            activity.binding.content.removeView(composeView)
+        }
+        snapshotter.record(bitmap, name, "$variant - Hardware")
+        bitmapPool.release(bitmap)
+    }
+
+    LottieCompositionCache.getInstance().clear()
 }
\ No newline at end of file
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeScaleTypesTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeScaleTypesTestCase.kt
new file mode 100644
index 0000000..fa99b35
--- /dev/null
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeScaleTypesTestCase.kt
@@ -0,0 +1,158 @@
+package com.airbnb.lottie.snapshots.tests
+
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.snapshots.SnapshotTestCase
+import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
+import com.airbnb.lottie.snapshots.loadCompositionFromAssetsSync
+import com.airbnb.lottie.snapshots.snapshotComposable
+
+class ComposeScaleTypesTestCase : SnapshotTestCase {
+    override suspend fun SnapshotTestCaseContext.run() {
+        val composition = loadCompositionFromAssetsSync("Lottie Logo 1.json")
+        snapshotComposable("Compose Scale Types", "Wrap Content", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                renderMode = renderMode,
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "720p", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(720.dp, 1280.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300@2x", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+                    .scale(2f)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300@4x", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+                    .scale(4f)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300 Crop", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Crop,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300 Inside", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Inside,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300 FillBounds", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.FillBounds,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300 Fit 2x", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Fit,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+                    .scale(2f)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x300 Crop 2x", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Crop,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 300.dp)
+                    .scale(2f)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "600x600 Inside", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Inside,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(600.dp, 600.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "600x600 FillBounds", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.FillBounds,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(600.dp, 600.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "600x600 Fit", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.Fit,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(600.dp, 600.dp)
+            )
+        }
+
+        snapshotComposable("Compose Scale Types", "300x600 FitBounds", renderHardwareAndSoftware = true) { renderMode ->
+            LottieAnimation(
+                composition,
+                1f,
+                contentScale = ContentScale.FillBounds,
+                renderMode = renderMode,
+                modifier = Modifier
+                    .size(300.dp, 600.dp)
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ScaleTypesTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ScaleTypesTestCase.kt
index 6cf274d..c012594 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ScaleTypesTestCase.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ScaleTypesTestCase.kt
@@ -11,7 +11,7 @@
 
 class ScaleTypesTestCase : SnapshotTestCase {
     override suspend fun SnapshotTestCaseContext.run() {
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "Wrap Content") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "Wrap Content", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = ViewGroup.LayoutParams.WRAP_CONTENT
@@ -19,7 +19,7 @@
             }
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "Match Parent") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "Match Parent", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = ViewGroup.LayoutParams.MATCH_PARENT
@@ -27,7 +27,7 @@
             }
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@2x") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@2x", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -36,7 +36,7 @@
             animationView.scale = 2f
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@4x") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@4x", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -45,7 +45,7 @@
             animationView.scale = 4f
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -54,7 +54,7 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_CROP
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerInside") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerInside", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -63,7 +63,7 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 fitXY") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 fitXY", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -72,17 +72,7 @@
             animationView.scaleType = ImageView.ScaleType.FIT_XY
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 fitXY DisableExtraScale") { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.disableExtraScaleModeInFitXY()
-            animationView.scaleType = ImageView.ScaleType.FIT_XY
-        }
-
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerInside @2x") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerInside @2x", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -92,7 +82,7 @@
             animationView.scale = 2f
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop @2x") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop @2x", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -102,7 +92,7 @@
             animationView.scale = 2f
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 centerInside") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 centerInside", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 600.dp.toInt()
@@ -111,7 +101,7 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 fitXY") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 fitXY", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 600.dp.toInt()
@@ -120,17 +110,7 @@
             animationView.scaleType = ImageView.ScaleType.FIT_XY
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 fitXY DisableExtraScale") { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 600.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.disableExtraScaleModeInFitXY()
-            animationView.scaleType = ImageView.ScaleType.FIT_XY
-        }
-
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x600 centerInside") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x600 centerInside", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -139,7 +119,7 @@
             animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x600 fitXY") { animationView ->
+        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x600 fitXY", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
                 width = 300.dp.toInt()
@@ -147,16 +127,6 @@
             }
             animationView.scaleType = ImageView.ScaleType.FIT_XY
         }
-
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x600 fitXY DisableExtraScale") { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 600.dp.toInt()
-            }
-            animationView.disableExtraScaleModeInFitXY()
-            animationView.scaleType = ImageView.ScaleType.FIT_XY
-        }
     }
 
     private val Number.dp get() = this.toFloat() / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)