Move full setRenderMode APIs to LottieDrawable (#2008)

Instead of asymmetric APIs where setRenderMode is on LottieAnimationView and useSoftwareRendering was on LottieDrawable, they now have parallel APIs.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07c2146..b9aeb06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 # 5.0.0
 ### New Features
+* [Removed API] Removed the `setScale(float)` APIs from `LottieAnimationView` and `LottieDrawable`. The expected behavior was highly ambiguous when paired with other scale types and canvas transformations. For the vast majority of cases, ImageView.ScaleType should be sufficient. For remaining cases, you may apply transformations to Canvas and use `LottieDrawable#draw` directly. 
 * Added support for the "Rounded Corners" effect on Shape and Rect layers ([#1953](https://github.com/airbnb/lottie-android/pull/1953))
 * Prior to 5.0, LottieAnimationView would _always_ call [setLayerType](https://developer.android.com/reference/android/view/View#setLayerType(int,%20android.graphics.Paint)) with either [HARDWARE](https://developer.android.com/reference/android/view/View#LAYER_TYPE_HARDWARE) or [SOFTWARE](https://developer.android.com/reference/android/view/View#LAYER_TYPE_SOFTWARE). In the hardware case, this would case Android to allocate a dedicated hardware buffer for the animation that had to be uploaded to the GPU separately. In the software case, LottieAnimationView would rely on View's internal [drawing cache](https://developer.android.com/reference/android/view/View#isDrawingCacheEnabled()).
 
@@ -13,13 +14,8 @@
 
   * Reduced memory consumption. In the hardware case, no new memory is allocated. In the software case, Lottie will create a bitmap that is the intersection of your View/Composition bounds mapped with the drawing transformation which often yields a surface are that is smaller than the entire LottieAnimationView.
   * lottie-compose now supports setting a RenderMode.
-  * Custom uses of LottieDrawable now support setting a RenderMode via [useSoftwareRendering](https://github.com/airbnb/lottie-android/blob/c5b8318c7cf205e95db143955acbfc69f86bc339/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java#L329).
-  * Lottie can now render outside of its composition bounds via [setClipToCompositionBounds](https://github.com/airbnb/lottie-android/blob/c5b8318c7cf205e95db143955acbfc69f86bc339/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java#L218).
-    Unless you are using one of the new APIs, you should not have to change anything in your code as a result of this page. It is intended to be an entirely internal implementation detail
-    that should improve performance by default and allow for new functionality.
-    
-    Please report any bugs or unexpected behavior that you experience as a result of this change.
-    [#1952](https://github.com/airbnb/lottie-android/pull/1952), [#1973](https://github.com/airbnb/lottie-android/pull/1973).
+  * Custom uses of LottieDrawable now support setting a RenderMode via [setRenderMode](https://github.com/airbnb/lottie-android/blob/c5b8318c7cf205e95db143955acbfc69f86bc339/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java#L329).
+* Lottie can now render outside of its composition bounds. To allow this with views such as LottieAnimationView, set `clipToCompositionBounds` to false on `LottieDrawable` or `LottieAnimationView` and `clipChildren` to false on the parent view. For Compose, use the `clipToCompositionBounds` parameter.
 * Prior to 5.0, LottieAnimationView handled all animation controls when the view's visibility or attach state changed. This worked fine for consumers of LottieAnimationView. However, custom uses of LottieDrawable were prone to leaking infinite animators if they did not properly handle cancelling animations correctly. This opens up the possibility for unexpected behavior and increased battery drain. Lottie now behaves more like animated drawables in the platform and moves this logic into the Drawable via its [setVisible](https://developer.android.com/reference/android/graphics/drawable/Drawable#setVisible(boolean,%20boolean)) API. This should lead to no explicit behavior changes if you are using LottieAnimationView. However, if you are using LottieDrawable directly and were explicitly pausing/cancelling animations on lifecycle changes, you may want to cross check your expected behavior with that of LottieDrawable after this update. This change also resolved a long standing bug when Lottie is used in RecyclerViews due to the complex way in which RecyclerView handles View lifecycles ([#1495](https://github.com/airbnb/lottie-android/issues/1495)).
   [#1981](https://github.com/airbnb/lottie-android/issues/1981)
 * Add an API [setClipToCompositionBounds](https://github.com/airbnb/lottie-android/blob/c5b8318c7cf205e95db143955acbfc69f86bc339/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java#L218) on LottieAnimationView, LottieDrawable, and the LottieAnimation composable to prevent Lottie from clipping animations to the composition bounds.
diff --git a/CHANGELOG_COMPOSE.md b/CHANGELOG_COMPOSE.md
index c7ce81f..38264c0 100644
--- a/CHANGELOG_COMPOSE.md
+++ b/CHANGELOG_COMPOSE.md
@@ -1,4 +1,5 @@
 # 5.0.0
+* You can tell Lottie to render the full animation even if it extends beyond the original composition bounds by setting `clipToCompositionBounds` to false.
 * Add support for dynamic text via `LottieProperty.TEXT` and the existing dynamic property APIs ([#1995](https://github.com/airbnb/lottie-android/issues/1995))
 * Respect Android system animator scale (include 0 for modes like battery saver) ([#2000](https://github.com/airbnb/lottie-android/pull/2000))
 * Added support for loading animations via content provider URIs (LottieCompositionSpec.ContentProvider) ([#1982](https://github.com/airbnb/lottie-android/pull/1982))
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 7a35356..f22be00 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
@@ -1,7 +1,6 @@
 package com.airbnb.lottie.compose
 
 import android.graphics.Matrix
-import android.os.Build
 import androidx.annotation.FloatRange
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.layout.Box
@@ -85,15 +84,13 @@
     val drawable = remember { LottieDrawable() }
     val matrix = remember { Matrix() }
     var setDynamicProperties: LottieDynamicProperties? by remember { mutableStateOf(null) }
-    val useSoftwareRendering: Boolean = remember(renderMode, composition) {
-        renderMode.useSoftwareRendering(Build.VERSION.SDK_INT, composition?.hasDashPattern() ?: false, composition?.maskAndMatteCount ?: 0)
-    }
 
     if (composition == null || composition.duration == 0f) return Box(modifier)
 
+    val dpScale = Utils.dpScale()
     Canvas(
         modifier = modifier
-            .size((composition.bounds.width() / Utils.dpScale()).dp, (composition.bounds.height() / Utils.dpScale()).dp)
+            .size((composition.bounds.width() / dpScale).dp, (composition.bounds.height() / dpScale).dp)
     ) {
         drawIntoCanvas { canvas ->
             val compositionSize = Size(composition.bounds.width().toFloat(), composition.bounds.height().toFloat())
@@ -106,6 +103,7 @@
             matrix.preScale(scale.scaleX, scale.scaleY)
 
             drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
+            drawable.renderMode = renderMode
             drawable.composition = composition
             if (dynamicProperties !== setDynamicProperties) {
                 setDynamicProperties?.removeFrom(drawable)
@@ -114,7 +112,6 @@
             }
             drawable.setOutlineMasksAndMattes(outlineMasksAndMattes)
             drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers
-            drawable.useSoftwareRendering(useSoftwareRendering)
             drawable.maintainOriginalImageBounds = maintainOriginalImageBounds
             drawable.clipToCompositionBounds = clipToCompositionBounds
             drawable.progress = progress
diff --git a/lottie/build.gradle b/lottie/build.gradle
index bcef0ef..8ba8dcf 100644
--- a/lottie/build.gradle
+++ b/lottie/build.gradle
@@ -38,6 +38,7 @@
 
   annotationProcessor "com.uber.nullaway:nullaway:0.9.2"
   errorprone "com.google.errorprone:error_prone_core:2.9.0"
+  //noinspection GradleDynamicVersion
   errorproneJavac "com.google.errorprone:javac:9+181-r4173-1"
 
   testImplementation "org.mockito:mockito-core:$mockitoVersion"
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 011731a..37c83a5 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -88,7 +88,6 @@
   @DrawableRes private int fallbackResource = 0;
 
   private final LottieDrawable lottieDrawable = new LottieDrawable();
-  private boolean isInitialized;
   private String animationName;
   private @RawRes int animationResId;
 
@@ -100,21 +99,11 @@
 
   private boolean autoPlay = false;
   private boolean cacheComposition = true;
-  private RenderMode renderMode = RenderMode.AUTOMATIC;
-  private boolean useSoftwareRendering = false;
   /**
    * Keeps track of explicit user actions taken and prevents onRestoreInstanceState from overwriting already set values.
    */
   private final Set<UserActionTaken> userActionsTaken = new HashSet<>();
   private final Set<LottieOnCompositionLoadedListener> lottieOnCompositionLoadedListeners = new HashSet<>();
-  /**
-   * Prevents a StackOverflowException on 4.4 in which getDrawingCache() calls buildDrawingCache().
-   * This isn't a great solution but it works and has very little performance overhead.
-   * At some point in the future, the original goal of falling back to hardware rendering when
-   * the animation is set to software rendering but it is too large to fit in a software bitmap
-   * should be reevaluated.
-   */
-  private int buildDrawingCacheDepth = 0;
 
   @Nullable private LottieTask<LottieComposition> compositionTask;
   /**
@@ -202,9 +191,6 @@
       LottieValueCallback<ColorFilter> callback = new LottieValueCallback<>(filter);
       addValueCallback(keyPath, LottieProperty.COLOR_FILTER, callback);
     }
-    if (ta.hasValue(R.styleable.LottieAnimationView_lottie_scale)) {
-      lottieDrawable.setScale(ta.getFloat(R.styleable.LottieAnimationView_lottie_scale, 1f));
-    }
 
     if (ta.hasValue(R.styleable.LottieAnimationView_lottie_renderMode)) {
       int renderModeOrdinal = ta.getInt(R.styleable.LottieAnimationView_lottie_renderMode, RenderMode.AUTOMATIC.ordinal());
@@ -224,9 +210,6 @@
     ta.recycle();
 
     lottieDrawable.setSystemAnimationsAreEnabled(Utils.getAnimationScale(getContext()) != 0f);
-
-    computeRenderMode();
-    isInitialized = true;
   }
 
   @Override public void setImageResource(int resId) {
@@ -553,7 +536,6 @@
     ignoreUnschedule = true;
     boolean isNewComposition = lottieDrawable.setComposition(composition);
     ignoreUnschedule = false;
-    computeRenderMode();
     if (getDrawable() == lottieDrawable && !isNewComposition) {
       // We can avoid re-setting the drawable, and invalidating the view, since the composition
       // hasn't changed.
@@ -602,7 +584,6 @@
   public void playAnimation() {
     userActionsTaken.add(UserActionTaken.PLAY_OPTION);
     lottieDrawable.playAnimation();
-    computeRenderMode();
   }
 
   /**
@@ -613,7 +594,6 @@
   public void resumeAnimation() {
     userActionsTaken.add(UserActionTaken.PLAY_OPTION);
     lottieDrawable.resumeAnimation();
-    computeRenderMode();
   }
 
   /**
@@ -955,41 +935,16 @@
     });
   }
 
-  /**
-   * Set the scale on the current composition. The only cost of this function is re-rendering the
-   * current frame so you may call it frequent to scale something up or down.
-   * <p>
-   * The smaller the animation is, the better the performance will be. You may find that scaling an
-   * animation down then rendering it in a larger ImageView and letting ImageView scale it back up
-   * with a scaleType such as centerInside will yield better performance with little perceivable
-   * quality loss.
-   * <p>
-   * You can also use a fixed view width/height in conjunction with the normal ImageView
-   * scaleTypes centerCrop and centerInside.
-   */
-  public void setScale(float scale) {
-    lottieDrawable.setScale(scale);
-    if (getDrawable() == lottieDrawable) {
-      setLottieDrawable();
-    }
-  }
-
-  public float getScale() {
-    return lottieDrawable.getScale();
-  }
-
   @MainThread
   public void cancelAnimation() {
     userActionsTaken.add(UserActionTaken.PLAY_OPTION);
     lottieDrawable.cancelAnimation();
-    computeRenderMode();
   }
 
   @MainThread
   public void pauseAnimation() {
     autoPlay = false;
     lottieDrawable.pauseAnimation();
-    computeRenderMode();
   }
 
   /**
@@ -1051,26 +1006,6 @@
   }
 
   /**
-   * 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.
-   *
-   * @see #setRenderMode(RenderMode)
-   * @see com.airbnb.lottie.LottieDrawable#draw(android.graphics.Canvas)
-   */
-  @Override
-  public void buildDrawingCache(boolean autoScale) {
-    L.beginSection("buildDrawingCache");
-    buildDrawingCacheDepth++;
-    super.buildDrawingCache(autoScale);
-    if (buildDrawingCacheDepth == 1 && getWidth() > 0 && getHeight() > 0 &&
-        getLayerType() == LAYER_TYPE_SOFTWARE && getDrawingCache(autoScale) == null) {
-      setRenderMode(RenderMode.HARDWARE);
-    }
-    buildDrawingCacheDepth--;
-    L.endSection("buildDrawingCache");
-  }
-
-  /**
    * Call this to set whether or not to render with hardware or software acceleration.
    * Lottie defaults to Automatic which will use hardware acceleration unless:
    * 1) There are dash paths and the device is pre-Pie.
@@ -1084,12 +1019,10 @@
    * should test both render modes. You should also test on pre-Pie and Pie+ devices
    * because the underlying rendering engine changed significantly.
    *
-   * @see LottieDrawable#useSoftwareRendering(boolean)
    * @see <a href="https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported">Android Hardware Acceleration</a>
    */
   public void setRenderMode(RenderMode renderMode) {
-    this.renderMode = renderMode;
-    computeRenderMode();
+    lottieDrawable.setRenderMode(renderMode);
   }
 
   /**
@@ -1097,17 +1030,7 @@
    * When the render mode is set to AUTOMATIC, the value will be derived from {@link RenderMode#useSoftwareRendering(int, boolean, int)}.
    */
   public RenderMode getRenderMode() {
-    return useSoftwareRendering ? RenderMode.SOFTWARE : RenderMode.HARDWARE;
-  }
-
-  private void computeRenderMode() {
-    LottieComposition composition = this.composition;
-    if (composition == null) {
-      return;
-    }
-    useSoftwareRendering = renderMode.useSoftwareRendering(
-        Build.VERSION.SDK_INT, composition.hasDashPattern(), composition.getMaskAndMatteCount());
-    lottieDrawable.useSoftwareRendering(useSoftwareRendering);
+    return lottieDrawable.getRenderMode();
   }
 
   /**
@@ -1157,7 +1080,6 @@
     // if the composition changes.
     setImageDrawable(null);
     setImageDrawable(lottieDrawable);
-    computeRenderMode();
     if (wasAnimating) {
       // This is necessary because lottieDrawable will get unscheduled and canceled when the drawable is set to null.
       lottieDrawable.resumeAnimation();
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index 10303c7..0c0ee78 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -6,13 +6,10 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Typeface;
@@ -20,6 +17,8 @@
 import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
 import android.widget.ImageView;
 
 import androidx.annotation.FloatRange;
@@ -65,7 +64,7 @@
 
   /**
    * Internal record keeping of the desired play state when {@link #isVisible()} transitions to or is false.
-   *
+   * <p>
    * If the animation was playing when it becomes invisible or play/pause is called on it while it is invisible, it will
    * store the state and then take the appropriate action when the drawable becomes visible again.
    */
@@ -77,7 +76,6 @@
 
   private LottieComposition composition;
   private final LottieValueAnimator animator = new LottieValueAnimator();
-  private float scale = 1f;
 
   // Call animationsEnabled() instead of using these fields directly.
   private boolean systemAnimationsEnabled = true;
@@ -121,11 +119,16 @@
   private boolean outlineMasksAndMattes;
   private boolean isApplyingOpacityToLayersEnabled;
 
+  private RenderMode renderMode = RenderMode.AUTOMATIC;
+  /**
+   * The actual render mode derived from {@link #renderMode}.
+   */
+  private boolean useSoftwareRendering = false;
   private final Matrix renderingMatrix = new Matrix();
-  private boolean softwareRenderingEnabled = false;
   private Bitmap softwareRenderingBitmap;
-  private final LPaint softwareRenderingClearPaint = new LPaint();
-  private final Canvas softwareRenderingCanvas = new Canvas();
+  private Canvas softwareRenderingCanvas;
+  private Rect canvasClipBounds;
+  private RectF canvasClipBoundsRectF;
   private Paint softwareRenderingPaint;
   private Rect softwareRenderingSrcBoundsRect;
   private Rect softwareRenderingDstBoundsRect;
@@ -218,6 +221,10 @@
   public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
     if (clipToCompositionBounds != this.clipToCompositionBounds) {
       this.clipToCompositionBounds = clipToCompositionBounds;
+      CompositionLayer compositionLayer = this.compositionLayer;
+      if (compositionLayer != null) {
+        compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
+      }
       invalidateSelf();
     }
   }
@@ -258,7 +265,7 @@
   /**
    * When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
    * When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
-   *
+   * <p>
    * Defaults to false.
    */
   public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) {
@@ -268,7 +275,7 @@
   /**
    * When true, dynamically set bitmaps will be drawn with the exact bounds of the original animation, regardless of the bitmap size.
    * When false, dynamically set bitmaps will be drawn at the top left of the original image but with its own bounds.
-   *
+   * <p>
    * Defaults to false.
    */
   public boolean getMaintainOriginalImageBounds() {
@@ -291,7 +298,6 @@
     buildCompositionLayer();
     animator.setComposition(composition);
     setProgress(animator.getAnimatedFraction());
-    setScale(scale);
 
     // We copy the tasks to a new ArrayList so that if this method is called from multiple threads,
     // then there won't be two iterators iterating and removing at the same time.
@@ -308,6 +314,7 @@
     lazyCompositionTasks.clear();
 
     composition.setPerformanceTrackingEnabled(performanceTrackingEnabled);
+    computeRenderMode();
 
     // Ensure that ImageView updates the drawable width/height so it can
     // properly calculate its drawable matrix.
@@ -321,17 +328,41 @@
   }
 
   /**
-   * When set to true, Lottie will first render your animation to a bitmap and then draw the bitmap
-   * onto the original canvas.
+   * Call this to set whether or not to render with hardware or software acceleration.
+   * Lottie defaults to Automatic which will use hardware acceleration unless:
+   * 1) There are dash paths and the device is pre-Pie.
+   * 2) There are more than 4 masks and mattes and the device is pre-Pie.
+   * Hardware acceleration is generally faster for those devices unless
+   * there are many large mattes and masks in which case there is a lot
+   * of GPU uploadTexture thrashing which makes it much slower.
+   * <p>
+   * In most cases, hardware rendering will be faster, even if you have mattes and masks.
+   * However, if you have multiple mattes and masks (especially large ones), you
+   * should test both render modes. You should also test on pre-Pie and Pie+ devices
+   * because the underlying rendering engine changed significantly.
    *
-   * @see LottieAnimationView#setRenderMode(RenderMode)
+   * @see <a href="https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported">Android Hardware Acceleration</a>
    */
-  public void useSoftwareRendering(boolean softwareRenderingEnabled) {
-    if (this.softwareRenderingEnabled == softwareRenderingEnabled) {
+  public void setRenderMode(RenderMode renderMode) {
+    this.renderMode = renderMode;
+    computeRenderMode();
+  }
+
+  /**
+   * Returns the actual render mode being used. It will always be {@link RenderMode#HARDWARE} or {@link RenderMode#SOFTWARE}.
+   * When the render mode is set to AUTOMATIC, the value will be derived from {@link RenderMode#useSoftwareRendering(int, boolean, int)}.
+   */
+  public RenderMode getRenderMode() {
+    return useSoftwareRendering ? RenderMode.SOFTWARE : RenderMode.HARDWARE;
+  }
+
+  private void computeRenderMode() {
+    LottieComposition composition = this.composition;
+    if (composition == null) {
       return;
     }
-    this.softwareRenderingEnabled = softwareRenderingEnabled;
-    invalidateSelf();
+    useSoftwareRendering = renderMode.useSoftwareRendering(
+        Build.VERSION.SDK_INT, composition.hasDashPattern(), composition.getMaskAndMatteCount());
   }
 
   public void setPerformanceTrackingEnabled(boolean enabled) {
@@ -403,6 +434,7 @@
     if (outlineMasksAndMattes) {
       compositionLayer.setOutlineMasksAndMattes(true);
     }
+    compositionLayer.setClipToCompositionBounds(clipToCompositionBounds);
   }
 
   public void clearComposition() {
@@ -470,39 +502,47 @@
 
     if (safeMode) {
       try {
-        drawInternal(canvas);
+        if (useSoftwareRendering) {
+          renderAndDrawAsBitmap(canvas, compositionLayer);
+        } else {
+          drawDirectlyToCanvas(canvas);
+        }
       } catch (Throwable e) {
         Logger.error("Lottie crashed in draw!", e);
       }
     } else {
-      drawInternal(canvas);
+      if (useSoftwareRendering) {
+        renderAndDrawAsBitmap(canvas, compositionLayer);
+      } else {
+        drawDirectlyToCanvas(canvas);
+      }
     }
 
     L.endSection("Drawable#draw");
   }
 
-  private void drawInternal(@NonNull Canvas canvas) {
-    if (!boundsMatchesCompositionAspectRatio()) {
-      drawWithNewAspectRatio(canvas);
-    } else {
-      drawWithOriginalAspectRatio(canvas);
-    }
-    isDirty = false;
-  }
-
-  private boolean boundsMatchesCompositionAspectRatio() {
+  /**
+   * To be used by lottie-compose only.
+   */
+  @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+  public void draw(Canvas canvas, Matrix matrix) {
+    CompositionLayer compositionLayer = this.compositionLayer;
     LottieComposition composition = this.composition;
-    if (composition == null || getBounds().isEmpty()) {
-      return true;
+    if (compositionLayer == null || composition == null) {
+      return;
     }
-    return aspectRatio(getBounds()) == aspectRatio(composition.getBounds());
+
+    if (useSoftwareRendering) {
+      canvas.save();
+      canvas.concat(matrix);
+      renderAndDrawAsBitmap(canvas, compositionLayer);
+      canvas.restore();
+    } else {
+      compositionLayer.draw(canvas, matrix, alpha);
+    }
   }
 
-  private float aspectRatio(Rect rect) {
-    return rect.width() / (float) rect.height();
-  }
-
-// <editor-fold desc="animator">
+  // <editor-fold desc="animator">
 
   @MainThread
   @Override
@@ -536,6 +576,7 @@
       return;
     }
 
+    computeRenderMode();
     if (animationsEnabled() || getRepeatCount() == 0) {
       if (isVisible()) {
         animator.playAnimation();
@@ -572,6 +613,7 @@
       return;
     }
 
+    computeRenderMode();
     if (animationsEnabled() || getRepeatCount() == 0) {
       if (isVisible()) {
         animator.resumeAnimation();
@@ -948,22 +990,6 @@
   }
 
   /**
-   * Set the scale on the current composition. The only cost of this function is re-rendering the
-   * current frame so you may call it frequent to scale something up or down.
-   * <p>
-   * The smaller the animation is, the better the performance will be. You may find that scaling an
-   * animation down then rendering it in a larger ImageView and letting ImageView scale it back up
-   * with a scaleType such as centerInside will yield better performance with little perceivable
-   * quality loss.
-   * <p>
-   * You can also use a fixed view width/height in conjunction with the normal ImageView
-   * scaleTypes centerCrop and centerInside.
-   */
-  public void setScale(float scale) {
-    this.scale = scale;
-  }
-
-  /**
    * Use this if you can't bundle images with your app. This may be useful if you download the
    * animations from the network or have the images saved to an SD Card. In that case, Lottie
    * will defer the loading of the bitmap to this delegate.
@@ -1004,10 +1030,6 @@
     return textDelegate == null && composition.getCharacters().size() > 0;
   }
 
-  public float getScale() {
-    return scale;
-  }
-
   public LottieComposition getComposition() {
     return composition;
   }
@@ -1035,12 +1057,12 @@
 
   @Override
   public int getIntrinsicWidth() {
-    return composition == null ? -1 : (int) (composition.getBounds().width() * getScale());
+    return composition == null ? -1 : composition.getBounds().width();
   }
 
   @Override
   public int getIntrinsicHeight() {
-    return composition == null ? -1 : (int) (composition.getBounds().height() * getScale());
+    return composition == null ? -1 : composition.getBounds().height();
   }
 
   /**
@@ -1156,7 +1178,7 @@
   /**
    * Returns the bitmap that will be rendered for the given id in the Lottie animation file.
    * The id is the asset reference id stored in the "id" property of each object in the "assets" array.
-   *
+   * <p>
    * The returned bitmap could be from:
    * * Embedded in the animation file as a base64 string.
    * * In the same directory as the animation file.
@@ -1176,7 +1198,7 @@
   /**
    * Returns the {@link LottieImageAsset} that will be rendered for the given id in the Lottie animation file.
    * The id is the asset reference id stored in the "id" property of each object in the "assets" array.
-   *
+   * <p>
    * The returned bitmap could be from:
    * * Embedded in the animation file as a base64 string.
    * * In the same directory as the animation file.
@@ -1300,67 +1322,33 @@
     callback.unscheduleDrawable(this, what);
   }
 
-  @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-  public void draw(Canvas canvas, Matrix matrix) {
+  /**
+   * Hardware accelerated render path.
+   */
+  private void drawDirectlyToCanvas(Canvas canvas) {
     CompositionLayer compositionLayer = this.compositionLayer;
     LottieComposition composition = this.composition;
     if (compositionLayer == null || composition == null) {
       return;
     }
 
-    if (softwareRenderingEnabled) {
-      canvas.save();
-      canvas.concat(matrix);
-      renderAndDrawAsBitmap(canvas, compositionLayer);
-      canvas.restore();
-    } else {
-      compositionLayer.draw(canvas, matrix, alpha);
-    }
-  }
-
-  private void drawWithNewAspectRatio(Canvas canvas) {
-    CompositionLayer compositionLayer = this.compositionLayer;
-    LottieComposition composition = this.composition;
-    if (compositionLayer == null || composition == null) {
-      return;
-    }
-
-    if (softwareRenderingEnabled) {
-      renderAndDrawAsBitmap(canvas, compositionLayer);
-    } else {
-      Rect bounds = getBounds();
+    renderingMatrix.reset();
+    Rect bounds = getBounds();
+    if (!bounds.isEmpty()) {
       // In fitXY mode, the scale doesn't take effect.
       float scaleX = bounds.width() / (float) composition.getBounds().width();
       float scaleY = bounds.height() / (float) composition.getBounds().height();
 
-      renderingMatrix.reset();
       renderingMatrix.preScale(scaleX, scaleY);
-      compositionLayer.draw(canvas, renderingMatrix, alpha);
     }
-  }
-
-  private void drawWithOriginalAspectRatio(Canvas canvas) {
-    CompositionLayer compositionLayer = this.compositionLayer;
-    LottieComposition composition = this.composition;
-    float scale = this.scale;
-    if (compositionLayer == null || composition == null) {
-      return;
-    }
-
-    if (softwareRenderingEnabled) {
-      renderAndDrawAsBitmap(canvas, compositionLayer);
-    } else {
-      renderingMatrix.reset();
-      renderingMatrix.preScale(scale, scale);
-      compositionLayer.draw(canvas, renderingMatrix, alpha);
-    }
+    compositionLayer.draw(canvas, renderingMatrix, alpha);
   }
 
   /**
-   * This is the software rendering pipeline. This draws the animation to an internally managed bitmap
-   * and then draws the bitmap to the original canvas.
+   * Software accelerated render path.
    *
-   * @see LottieDrawable#useSoftwareRendering(boolean)
+   * This draws the animation to an internally managed bitmap and then draws the bitmap to the original canvas.
+   *
    * @see LottieAnimationView#setRenderMode(RenderMode)
    */
   private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer) {
@@ -1368,33 +1356,34 @@
 
     //noinspection deprecation
     originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
-    softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
-    renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
+
+    // Get the canvas clip bounds and map it to the coordinate space of canvas with it's current transform.
+    originalCanvas.getClipBounds(canvasClipBounds);
+    convertRect(canvasClipBounds, canvasClipBoundsRectF);
+    softwareRenderingOriginalCanvasMatrix.mapRect(canvasClipBoundsRectF);
+    convertRect(canvasClipBoundsRectF, canvasClipBounds);
+
+    if (clipToCompositionBounds) {
+      // Start with the intrinsic bounds. This will later be unioned with the clip bounds to find the
+      // smallest possible render area.
+      softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
+    } else {
+      // Calculate the full bounds of the animation.
+      compositionLayer.getBounds(softwareRenderingTransformedBounds, null, false);
+    }
+    // Transform the animation bounds to the bounds that they will render to on the canvas.
+    softwareRenderingOriginalCanvasMatrix.mapRect(softwareRenderingTransformedBounds);
 
     // 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();
+    scaleRect(softwareRenderingTransformedBounds, scaleX, scaleY);
 
-    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);
+    if (!ignoreCanvasClipBounds()) {
+      softwareRenderingTransformedBounds.intersect(canvasClipBounds.left, canvasClipBounds.top, canvasClipBounds.right, canvasClipBounds.bottom);
     }
-    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);
-
 
     int renderWidth = (int) Math.ceil(softwareRenderingTransformedBounds.width());
     int renderHeight = (int) Math.ceil(softwareRenderingTransformedBounds.height());
@@ -1405,14 +1394,14 @@
 
     ensureSoftwareRenderingBitmap(renderWidth, renderHeight);
 
-    softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
-
     if (isDirty) {
-      softwareRenderingBitmap.eraseColor(0);
-      renderingMatrix.preScale(scale * scaleX, scale * scaleY);
+      renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
+      renderingMatrix.preScale(scaleX, 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);
+
+      softwareRenderingBitmap.eraseColor(0);
       compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
 
       // Calculate the dst bounds.
@@ -1420,35 +1409,42 @@
       // of the original canvas.
       // Take the bounds of the rendered animation and map them to the canvas's coordinates.
       // This is similar to the src rect above but the src bound may have a left and top offset.
+      softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
       softwareRenderingOriginalCanvasMatrixInverse.mapRect(softwareRenderingDstBoundsRectF, softwareRenderingTransformedBounds);
       convertRect(softwareRenderingDstBoundsRectF, softwareRenderingDstBoundsRect);
+      isDirty = false;
     }
+
+    softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
     originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingSrcBoundsRect, softwareRenderingDstBoundsRect, softwareRenderingPaint);
   }
 
   private void ensureSoftwareRenderingObjectsInitialized() {
-    if (softwareRenderingPaint != null) {
+    if (softwareRenderingCanvas != null) {
       return;
     }
-    softwareRenderingPaint = new LPaint();
-    softwareRenderingClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
-    softwareRenderingClearPaint.setColor(Color.BLACK);
-    softwareRenderingSrcBoundsRect = new Rect();
-    softwareRenderingDstBoundsRect = new Rect();
-    softwareRenderingDstBoundsRectF = new RectF();
+    softwareRenderingCanvas = new Canvas();
     softwareRenderingTransformedBounds = new RectF();
     softwareRenderingOriginalCanvasMatrix = new Matrix();
     softwareRenderingOriginalCanvasMatrixInverse = new Matrix();
+    canvasClipBounds = new Rect();
+    canvasClipBoundsRectF = new RectF();
+    softwareRenderingPaint = new LPaint();
+    softwareRenderingSrcBoundsRect = new Rect();
+    softwareRenderingDstBoundsRect = new Rect();
+    softwareRenderingDstBoundsRectF = new RectF();
   }
 
   private void ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight) {
     if (softwareRenderingBitmap == null ||
         softwareRenderingBitmap.getWidth() < renderWidth ||
         softwareRenderingBitmap.getHeight() < renderHeight) {
+      // The bitmap is larger. We need to create a new one.
       softwareRenderingBitmap = Bitmap.createBitmap(renderWidth, renderHeight, Bitmap.Config.ARGB_8888);
       softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
       isDirty = true;
     } else if (softwareRenderingBitmap.getWidth() > renderWidth || softwareRenderingBitmap.getHeight() > renderHeight) {
+      // The bitmap is smaller. Take subset of the original.
       softwareRenderingBitmap = Bitmap.createBitmap(softwareRenderingBitmap, 0, 0, renderWidth, renderHeight);
       softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
       isDirty = true;
@@ -1466,4 +1462,43 @@
         (int) Math.ceil(src.bottom)
     );
   }
+
+  /**
+   * Convert a Rect to a RectF
+   */
+  private void convertRect(Rect src, RectF dst) {
+    dst.set(
+        src.left,
+        src.top,
+        src.right,
+        src.bottom);
+  }
+
+  private void scaleRect(RectF rect, float scaleX, float scaleY) {
+    rect.set(
+        rect.left * scaleX,
+        rect.top * scaleY,
+        rect.right * scaleX,
+        rect.bottom * scaleY
+    );
+  }
+
+  /**
+   * When a View's parent has clipChildren set to false, it doesn't affect the clipBound
+   * of its child canvases so we should explicitly check for it and draw the full animation
+   * bounds instead.
+   */
+  private boolean ignoreCanvasClipBounds() {
+    Callback callback = getCallback();
+    if (!(callback instanceof View)) {
+      // If the callback isn't a view then respect the canvas's clip bounds.
+      return false;
+    }
+    ViewParent parent = ((View) callback).getParent();
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && parent instanceof ViewGroup) {
+      return !((ViewGroup) parent).getClipChildren();
+    }
+    // Unlikely to ever happen. If the callback is a View, its parent should be a ViewGroup.
+    return false;
+  }
 }
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 43ea6a1..6ddd5f7 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
@@ -33,6 +33,8 @@
   @Nullable private Boolean hasMatte;
   @Nullable private Boolean hasMasks;
 
+  private boolean clipToCompositionBounds = true;
+
   public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
       LottieComposition composition) {
     super(lottieDrawable, layerModel);
@@ -88,6 +90,10 @@
     }
   }
 
+  public void setClipToCompositionBounds(boolean clipToCompositionBounds) {
+    this.clipToCompositionBounds = clipToCompositionBounds;
+  }
+
   @Override public void setOutlineMasksAndMattes(boolean outline) {
     super.setOutlineMasksAndMattes(outline);
     for (BaseLayer layer : layers) {
@@ -112,7 +118,9 @@
     int childAlpha = isDrawingWithOffScreen ? 255 : parentAlpha;
     for (int i = layers.size() - 1; i >= 0; i--) {
       boolean nonEmptyClip = true;
-      if (lottieDrawable.getClipToCompositionBounds() && !newClipRect.isEmpty()) {
+      // Only clip precomps. This mimics the way After Effects renders animations.
+      boolean ignoreClipOnThisLayer = !clipToCompositionBounds && "__container".equals(layerModel.getName());
+      if (!ignoreClipOnThisLayer && !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 73e3481..024c323 100644
--- a/lottie/src/main/res/values/attrs.xml
+++ b/lottie/src/main/res/values/attrs.xml
@@ -18,16 +18,15 @@
         <attr name="lottie_progress" format="float" />
         <attr name="lottie_enableMergePathsForKitKatAndAbove" format="boolean" />
         <attr name="lottie_colorFilter" format="color" />
-        <attr name="lottie_scale" format="float" />
         <attr name="lottie_speed" format="float" />
         <attr name="lottie_cacheComposition" format="boolean" />
         <attr name="lottie_ignoreDisabledSystemAnimations" format="boolean" />
+        <attr name="lottie_clipToCompositionBounds" format="boolean" />
         <!-- These values must be kept in sync with the RenderMode enum -->
         <attr name="lottie_renderMode" format="enum">
             <enum name="automatic" value="0" />
             <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/lottie/src/test/java/com/airbnb/lottie/BaseTest.java b/lottie/src/test/java/com/airbnb/lottie/BaseTest.java
index a672a0d..ee2e054 100644
--- a/lottie/src/test/java/com/airbnb/lottie/BaseTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/BaseTest.java
@@ -9,6 +9,6 @@
 
 @RunWith(RobolectricTestRunner.class)
 @Config(sdk = Build.VERSION_CODES.P)
-@Ignore
+@Ignore("Base Test")
 public class BaseTest {
 }
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
index f7f2e1a..804f961 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
@@ -47,7 +47,7 @@
   /**
    * This hangs on CI but not locally.
    */
-  @Ignore
+  @Ignore("hangs on ci")
   @Test
   public void testRemoveListener() {
     final Semaphore lock = new Semaphore(0);
diff --git a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
index 35cbe31..e715f5f 100644
--- a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
+++ b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerFragment.kt
@@ -45,7 +45,6 @@
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.snackbar.Snackbar
 import kotlin.math.abs
-import kotlin.math.min
 import kotlin.math.roundToInt
 
 class PlayerFragment : BaseMvRxFragment(R.layout.player_fragment) {
@@ -196,13 +195,6 @@
             binding.controlBarBackgroundColor.backgroundColorContainer.animateVisible(it)
         }
 
-        binding.controlBar.scaleToggle.setOnClickListener { viewModel.toggleScaleVisible() }
-        binding.controlBarScale.closeScaleButton.setOnClickListener { viewModel.setScaleVisible(false) }
-        viewModel.selectSubscribe(PlayerState::scaleVisible) {
-            binding.controlBar.scaleToggle.isActivated = it
-            binding.controlBarScale.scaleContainer.animateVisible(it)
-        }
-
         binding.controlBar.trimToggle.setOnClickListener { viewModel.toggleTrimVisible() }
         binding.controlBarTrim.closeTrimButton.setOnClickListener { viewModel.setTrimVisible(false) }
         viewModel.selectSubscribe(PlayerState::trimVisible) {
@@ -283,16 +275,6 @@
             binding.animationView.invalidate()
         }
 
-        binding.controlBarScale.scaleSeekBar.setOnSeekBarChangeListener(OnSeekBarChangeListenerAdapter(
-            onProgressChanged = { _, progress, _ ->
-                val minScale = minScale()
-                val maxScale = maxScale()
-                val scale = minScale + progress / 100f * (maxScale - minScale)
-                binding.animationView.scale = scale
-                binding.controlBarScale.scaleText.text = "%.0f%%".format(scale * 100)
-            }
-        ))
-
         arrayOf(
             binding.controlBarBackgroundColor.backgroundButton1,
             binding.controlBarBackgroundColor.backgroundButton2,
@@ -451,9 +433,6 @@
             binding.controlBarPlayerControls.renderTimesGraph.invalidate()
         }
 
-        // Scale up to fill the screen
-        binding.controlBarScale.scaleSeekBar.progress = 50
-
         binding.bottomSheetKeyPaths.keyPathsRecyclerView.buildModelsWith(object : EpoxyRecyclerView.ModelBuilderCallback {
             override fun buildModels(controller: EpoxyController) {
                 binding.animationView.resolveKeyPath(KeyPath("**")).forEachIndexed { index, keyPath ->
@@ -508,18 +487,6 @@
         )
     }
 
-    private fun minScale() = 0.05f
-
-    private fun maxScale(): Float = withState(viewModel) { state ->
-        val screenWidth = resources.displayMetrics.widthPixels.toFloat()
-        val screenHeight = resources.displayMetrics.heightPixels.toFloat()
-        val bounds = state.composition()?.bounds
-        return@withState min(
-            screenWidth / (bounds?.width()?.toFloat() ?: screenWidth),
-            screenHeight / (bounds?.height()?.toFloat() ?: screenHeight)
-        ) * 2f
-    }
-
     private fun beginDelayedTransition() = TransitionManager.beginDelayedTransition(binding.container, transition)
 
     companion object {
diff --git a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
index ec6e0e1..592573e 100644
--- a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
+++ b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
@@ -27,7 +27,6 @@
     val outlineMasksAndMattes: Boolean = false,
     val borderVisible: Boolean = false,
     val backgroundColorVisible: Boolean = false,
-    val scaleVisible: Boolean = false,
     val speedVisible: Boolean = false,
     val trimVisible: Boolean = false,
     val useMergePaths: Boolean = false,
@@ -79,10 +78,6 @@
 
     fun setBackgroundColorVisible(visible: Boolean) = setState { copy(backgroundColorVisible = visible) }
 
-    fun toggleScaleVisible() = setState { copy(scaleVisible = !scaleVisible) }
-
-    fun setScaleVisible(visible: Boolean) = setState { copy(scaleVisible = visible) }
-
     fun toggleSpeedVisible() = setState { copy(speedVisible = !speedVisible) }
 
     fun setSpeedVisible(visible: Boolean) = setState { copy(speedVisible = visible) }
@@ -112,7 +107,6 @@
             renderGraphVisible = false,
             borderVisible = false,
             backgroundColorVisible = false,
-            scaleVisible = false,
             speedVisible = false,
             trimVisible = false
         )
diff --git a/sample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt b/sample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
index 6789387..936bfd6 100644
--- a/sample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
+++ b/sample/src/main/kotlin/com/airbnb/lottie/samples/QRScanActivity.kt
@@ -14,6 +14,7 @@
 
 class QRScanActivity : AppCompatActivity(), QRCodeReaderView.OnQRCodeReadListener {
     private val binding: QrscanActivityBinding by viewBinding()
+    @Suppress("DEPRECATION")
     private val vibrator by lazy { getSystemService(Context.VIBRATOR_SERVICE) as Vibrator }
 
     // Sometimes the qr code is read twice in rapid succession. This prevents it from being read
diff --git a/sample/src/main/res/drawable/ic_scale.xml b/sample/src/main/res/drawable/ic_scale.xml
deleted file mode 100644
index ff394c0..0000000
--- a/sample/src/main/res/drawable/ic_scale.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M15,3l2.3,2.3 -2.89,2.87 1.42,1.42L18.7,6.7 21,9L21,3zM3,9l2.3,-2.3 2.87,2.89 1.42,-1.42L6.7,5.3 9,3L3,3zM9,21l-2.3,-2.3 2.89,-2.87 -1.42,-1.42L5.3,17.3 3,15v6zM21,15l-2.3,2.3 -2.87,-2.89 -1.42,1.42 2.89,2.87L15,21h6z"/>
-</vector>
diff --git a/sample/src/main/res/layout/control_bar.xml b/sample/src/main/res/layout/control_bar.xml
index b44548c..9bc1c46 100644
--- a/sample/src/main/res/layout/control_bar.xml
+++ b/sample/src/main/res/layout/control_bar.xml
@@ -29,11 +29,6 @@
             android:id="@+id/warningsButton"
             style="@style/ControlBarItem" />
         <com.airbnb.lottie.samples.views.ControlBarItemToggleView
-            android:id="@+id/scaleToggle"
-            style="@style/ControlBarItem"
-            app:src="@drawable/ic_scale"
-            app:text="@string/control_bar_scale" />
-        <com.airbnb.lottie.samples.views.ControlBarItemToggleView
             android:id="@+id/borderToggle"
             style="@style/ControlBarItem"
             app:src="@drawable/ic_device"
diff --git a/sample/src/main/res/layout/control_bar_scale.xml b/sample/src/main/res/layout/control_bar_scale.xml
deleted file mode 100644
index faa2994..0000000
--- a/sample/src/main/res/layout/control_bar_scale.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/scaleContainer"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:visibility="gone">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="64dp"
-        android:layout_gravity="center_vertical"
-        android:background="@android:color/white"
-        android:gravity="center_vertical"
-        android:orientation="horizontal">
-
-        <TextView
-            android:id="@+id/scaleText"
-            android:layout_width="64dp"
-            android:layout_height="wrap_content"
-            android:gravity="center"
-            android:textColor="#222222"
-            android:textSize="14sp"/>
-
-        <androidx.appcompat.widget.AppCompatSeekBar
-            android:id="@+id/scaleSeekBar"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginRight="8dp"
-            android:layout_weight="1"/>
-
-        <ImageButton
-            android:id="@+id/closeScaleButton"
-            android:layout_width="32dp"
-            android:layout_height="32dp"
-            android:layout_marginRight="16dp"
-            android:background="?attr/selectableItemBackgroundBorderless"
-            app:srcCompat="@drawable/ic_close"/>
-    </LinearLayout>
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="@dimen/divider_height"
-        android:layout_gravity="bottom"
-        android:background="@color/divider"/>
-</FrameLayout>
diff --git a/sample/src/main/res/layout/list_item_loader.xml b/sample/src/main/res/layout/list_item_loader.xml
index 02eb7e4..3c3c569 100644
--- a/sample/src/main/res/layout/list_item_loader.xml
+++ b/sample/src/main/res/layout/list_item_loader.xml
@@ -5,14 +5,13 @@
     android:layout_height="match_parent">
 
     <com.airbnb.lottie.LottieAnimationView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_width="100dp"
+        android:layout_height="100dp"
         android:layout_gravity="center"
         android:layout_marginBottom="16dp"
         android:layout_marginTop="16dp"
         app:lottie_autoPlay="true"
         app:lottie_loop="true"
-        app:lottie_rawRes="@raw/material_wave_loading"
-        app:lottie_scale="0.4"/>
+        app:lottie_rawRes="@raw/material_wave_loading"/>
 
 </merge>
\ No newline at end of file
diff --git a/sample/src/main/res/layout/player_fragment.xml b/sample/src/main/res/layout/player_fragment.xml
index 26c7bf6..573fbcc 100644
--- a/sample/src/main/res/layout/player_fragment.xml
+++ b/sample/src/main/res/layout/player_fragment.xml
@@ -45,10 +45,6 @@
             layout="@layout/control_bar_speed" />
 
         <include
-            android:id="@+id/control_bar_scale"
-            layout="@layout/control_bar_scale" />
-
-        <include
             android:id="@+id/control_bar_background_color"
             layout="@layout/control_bar_background_color" />
 
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 fec2593..0ef1f39 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
@@ -15,6 +15,7 @@
 import com.airbnb.lottie.model.LottieCompositionCache
 import com.airbnb.lottie.snapshots.tests.ApplyOpacityToLayerTestCase
 import com.airbnb.lottie.snapshots.tests.AssetsTestCase
+import com.airbnb.lottie.snapshots.tests.ClipChildrenTestCase
 import com.airbnb.lottie.snapshots.tests.ColorStateListColorFilterTestCase
 import com.airbnb.lottie.snapshots.tests.ComposeDynamicPropertiesTestCase
 import com.airbnb.lottie.snapshots.tests.ComposeScaleTypesTestCase
@@ -123,6 +124,7 @@
             LargeCompositionSoftwareRendering(),
             ComposeDynamicPropertiesTestCase(),
             ProdAnimationsTestCase(),
+            ClipChildrenTestCase(),
         )
 
         withTimeout(TimeUnit.MINUTES.toMillis(45)) {
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 0563c6a..35dc7e5 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
@@ -1,7 +1,6 @@
 package com.airbnb.lottie.snapshots
 
 import android.content.Context
-import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Canvas
 import android.graphics.Color
@@ -9,7 +8,6 @@
 import android.util.Log
 import android.view.View
 import android.view.ViewGroup
-import android.view.ViewTreeObserver
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
@@ -17,7 +15,6 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.ui.platform.ComposeView
-import androidx.core.view.doOnLayout
 import com.airbnb.lottie.FontAssetDelegate
 import com.airbnb.lottie.LottieAnimationView
 import com.airbnb.lottie.LottieComposition
@@ -86,7 +83,6 @@
     val animationView = animationViewPool.acquire()
     animationView.setComposition(composition)
     animationView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-    animationView.scale = 1f
     animationView.scaleType = ImageView.ScaleType.FIT_CENTER
     callback(animationView)
     val animationViewContainer = animationView.parent as ViewGroup
@@ -142,7 +138,6 @@
     callback: ((FilmStripView) -> Unit)? = null,
 ) = withContext(Dispatchers.Default) {
     log("Snapshotting $name")
-    val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
     val filmStripView = filmStripViewPool.acquire()
     filmStripView.setOutlineMasksAndMattes(false)
     filmStripView.setApplyingOpacityToLayersEnabled(false)
@@ -153,6 +148,7 @@
         }
     })
     callback?.invoke(filmStripView)
+    val spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
     filmStripView.measure(spec, spec)
     filmStripView.layout(0, 0, filmStripView.measuredWidth, filmStripView.measuredHeight)
     val bitmap = bitmapPool.acquire(filmStripView.width, filmStripView.height)
@@ -229,7 +225,6 @@
         bitmapPool.release(bitmap)
     }
 
-
     onActivity { activity ->
         activity.binding.content.removeView(composeView)
     }
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ClipChildrenTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ClipChildrenTestCase.kt
new file mode 100644
index 0000000..f2408e8
--- /dev/null
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ClipChildrenTestCase.kt
@@ -0,0 +1,79 @@
+package com.airbnb.lottie.snapshots.tests
+
+import android.graphics.Canvas
+import android.view.LayoutInflater
+import android.view.View
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.snapshots.SnapshotTestCase
+import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
+import com.airbnb.lottie.snapshots.databinding.ClipChildrenBinding
+import com.airbnb.lottie.snapshots.snapshotComposable
+
+class ClipChildrenTestCase : SnapshotTestCase {
+    override suspend fun SnapshotTestCaseContext.run() {
+        val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/BeyondBounds.json").value!!
+        snapshotComposable("Compose Clip Children", "Clip", renderHardwareAndSoftware = true) { renderMode ->
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier
+                    .size(400.dp, 400.dp)
+            ) {
+                LottieAnimation(
+                    composition,
+                    0.7f,
+                    contentScale = ContentScale.Crop,
+                    renderMode = renderMode,
+                    modifier = Modifier
+                        .size(200.dp, 100.dp)
+                )
+            }
+        }
+
+        snapshotComposable("Compose Clip Children", "Dont Clip", renderHardwareAndSoftware = true) { renderMode ->
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier
+                    .size(400.dp, 400.dp)
+            ) {
+                LottieAnimation(
+                    composition,
+                    0.7f,
+                    contentScale = ContentScale.Crop,
+                    renderMode = renderMode,
+                    clipToCompositionBounds = false,
+                    modifier = Modifier
+                        .size(200.dp, 100.dp)
+                )
+            }
+        }
+
+        val binding = ClipChildrenBinding.inflate(LayoutInflater.from(context))
+        binding.animationView.setComposition(composition)
+        binding.root.measureAndLayout()
+
+        val bitmap1 = bitmapPool.acquire(binding.root.width, binding.root.height)
+        val canvas = Canvas(bitmap1)
+        binding.root.draw(canvas)
+        snapshotter.record(bitmap1, "Clip Children", "Clip")
+
+        val bitmap2 = bitmapPool.acquire(binding.root.width, binding.root.height)
+        canvas.setBitmap(bitmap2)
+        binding.animationView.clipToCompositionBounds = false
+        binding.root.draw(canvas)
+        snapshotter.record(bitmap2, "Clip Children", "Don't Clip")
+
+    }
+
+    private fun View.measureAndLayout() {
+        val spec = View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
+        measure(spec, spec)
+        layout(0, 0, measuredWidth, measuredHeight)
+    }
+}
\ No newline at end of file
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/FailureTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/FailureTestCase.kt
index b9d07ae..a00e8a2 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/FailureTestCase.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/FailureTestCase.kt
@@ -19,7 +19,6 @@
         animationView.setAnimationFromJson("Not Valid Json", null)
         semaphore.acquire()
         animationView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-        animationView.scale = 1f
         animationView.scaleType = ImageView.ScaleType.FIT_CENTER
         val widthSpec = View.MeasureSpec.makeMeasureSpec(
             context.resources.displayMetrics
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/LargeCompositionSoftwareRendering.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/LargeCompositionSoftwareRendering.kt
index cdce72f..ae4fb16 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/LargeCompositionSoftwareRendering.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/LargeCompositionSoftwareRendering.kt
@@ -31,10 +31,6 @@
         snapshotWithImageView("Center") { av ->
             av.scaleType = ImageView.ScaleType.CENTER
         }
-        snapshotWithImageView("Center With Scale") { av ->
-            av.scaleType = ImageView.ScaleType.CENTER
-            av.scale = 0.08f
-        }
         snapshotWithImageView("CenterInside") { av ->
             av.scaleType = ImageView.ScaleType.CENTER_INSIDE
         }
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
index ba3dccd..903d319 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ProdAnimationsTestCase.kt
@@ -25,6 +25,10 @@
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.zip.ZipInputStream
 
+/**
+ * TODO:
+ * prod-com.eharmony-lottie-loader-data
+ */
 class ProdAnimationsTestCase : SnapshotTestCase {
     private val filesChannel = Channel<File>(capacity = 2_048)
 
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 c012594..a3393e4 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
@@ -27,24 +27,6 @@
             }
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@2x", renderHardwareAndSoftware = true) { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.scale = 2f
-        }
-
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300@4x", renderHardwareAndSoftware = true) { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.scale = 4f
-        }
-
         withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
@@ -72,26 +54,6 @@
             animationView.scaleType = ImageView.ScaleType.FIT_XY
         }
 
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerInside @2x", renderHardwareAndSoftware = true) { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.scaleType = ImageView.ScaleType.CENTER_INSIDE
-            animationView.scale = 2f
-        }
-
-        withAnimationView("Lottie Logo 1.json", "Scale Types", "300x300 centerCrop @2x", renderHardwareAndSoftware = true) { animationView ->
-            animationView.progress = 1f
-            animationView.updateLayoutParams {
-                width = 300.dp.toInt()
-                height = 300.dp.toInt()
-            }
-            animationView.scaleType = ImageView.ScaleType.CENTER_CROP
-            animationView.scale = 2f
-        }
-
         withAnimationView("Lottie Logo 1.json", "Scale Types", "600x300 centerInside", renderHardwareAndSoftware = true) { animationView ->
             animationView.progress = 1f
             animationView.updateLayoutParams {
@@ -129,5 +91,5 @@
         }
     }
 
-    private val Number.dp get() = this.toFloat() / (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
+    private val Number.dp get() = this.toFloat() * (Resources.getSystem().displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)
 }
\ No newline at end of file
diff --git a/snapshot-tests/src/main/assets/Tests/BeyondBounds.json b/snapshot-tests/src/main/assets/Tests/BeyondBounds.json
new file mode 100644
index 0000000..6f8593f
--- /dev/null
+++ b/snapshot-tests/src/main/assets/Tests/BeyondBounds.json
@@ -0,0 +1 @@
+{"v":"5.8.2","fr":23.9759979248047,"ip":0,"op":71.9999937681822,"w":400,"h":400,"nm":"BeyondBounds","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":[200,200,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":[510.547,510.547],"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":[0,0],"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":71.9999937681822,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/snapshot-tests/src/main/res/layout/clip_children.xml b/snapshot-tests/src/main/res/layout/clip_children.xml
new file mode 100644
index 0000000..e3ac409
--- /dev/null
+++ b/snapshot-tests/src/main/res/layout/clip_children.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="600px"
+    android:layout_height="600px"
+    android:clipChildren="false"
+    tools:ignore="PxUsage">
+
+    <com.airbnb.lottie.LottieAnimationView
+        android:id="@+id/animation_view"
+        android:layout_width="400px"
+        android:layout_height="200px"
+        android:layout_gravity="center"
+        android:scaleType="centerCrop" />
+
+</FrameLayout>
\ No newline at end of file