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