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)