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 ed4b10c5e..01a3c6a3b 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 @@ -54,6 +54,12 @@ import kotlin.math.roundToInt * features so it defaults to off. The only way to know if your animation will work * well with merge paths or not is to try it. If your animation has merge paths and * doesn't render correctly, please file an issue. + * @param renderMode Allows you to specify whether you want Lottie to use hardware or software rendering. + * Defaults to AUTOMATIC. Refer to [LottieAnimationView.setRenderMode] for more info. + * @param maintainOriginalImageBounds 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. + * Defaults to false. * @param dynamicProperties Allows you to change the properties of an animation dynamically. To use them, use * [rememberLottieDynamicProperties]. Refer to its docs for more info. * @param alignment Define where the animation should be placed within this composable if it has a different @@ -70,6 +76,7 @@ fun LottieAnimation( applyOpacityToLayers: Boolean = false, enableMergePaths: Boolean = false, renderMode: RenderMode = RenderMode.AUTOMATIC, + maintainOriginalImageBounds: Boolean = false, dynamicProperties: LottieDynamicProperties? = null, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, @@ -108,6 +115,7 @@ fun LottieAnimation( drawable.isApplyingOpacityToLayersEnabled = applyOpacityToLayers drawable.enableMergePathsForKitKatAndAbove(enableMergePaths) drawable.useSoftwareRendering(useSoftwareRendering) + drawable.maintainOriginalImageBounds = maintainOriginalImageBounds drawable.clipToCompositionBounds = clipToComposition drawable.progress = progress drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height()) @@ -136,6 +144,7 @@ fun LottieAnimation( applyOpacityToLayers: Boolean = false, enableMergePaths: Boolean = false, renderMode: RenderMode = RenderMode.AUTOMATIC, + maintainOriginalImageBounds: Boolean = false, dynamicProperties: LottieDynamicProperties? = null, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, @@ -157,6 +166,7 @@ fun LottieAnimation( applyOpacityToLayers, enableMergePaths, renderMode, + maintainOriginalImageBounds, dynamicProperties, alignment, contentScale, diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt index 3bec2455d..ea1331c84 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieDynamicProperties.kt @@ -19,6 +19,7 @@ import com.airbnb.lottie.value.ScaleXY * This takes a vararg of individual dynamic properties which should be created with [rememberLottieDynamicProperty]. * * @see rememberLottieDynamicProperty + * @see LottieDrawable.setMaintainOriginalImageBounds */ @Composable fun rememberLottieDynamicProperties( diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java index bd8e56097..20d30cf8a 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java @@ -843,6 +843,26 @@ public String getImageAssetsFolder() { return lottieDrawable.getImageAssetsFolder(); } + /** + * 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. + * + * Defaults to false. + */ + public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) { + lottieDrawable.setMaintainOriginalImageBounds(maintainOriginalImageBounds); + } + + /** + * 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. + * + * Defaults to false. + */ + public boolean getMaintainOriginalImageBounds() { + return lottieDrawable.getMaintainOriginalImageBounds(); + } + /** * Allows you to modify or clear a bitmap that was loaded for an image either automatically * through {@link #setImageAssetsFolder(String)} or with an {@link ImageAssetDelegate}. diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java index d801f8009..10303c791 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 @@ public void onAnimationUpdate(ValueAnimator animation) { @Nullable TextDelegate textDelegate; private boolean enableMergePaths; + private boolean maintainOriginalImageBounds = false; private boolean clipToCompositionBounds = true; @Nullable private CompositionLayer compositionLayer; @@ -254,6 +255,26 @@ public String getImageAssetsFolder() { return imageAssetsFolder; } + /** + * 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. + * + * Defaults to false. + */ + public void setMaintainOriginalImageBounds(boolean maintainOriginalImageBounds) { + this.maintainOriginalImageBounds = maintainOriginalImageBounds; + } + + /** + * 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. + * + * Defaults to false. + */ + public boolean getMaintainOriginalImageBounds() { + return maintainOriginalImageBounds; + } + /** * Create a composition with {@link LottieCompositionFactory} * @@ -1115,7 +1136,11 @@ public Bitmap updateBitmap(String id, @Nullable Bitmap bitmap) { return ret; } + /** + * @deprecated use {@link #getBitmapForId(String)}. + */ @Nullable + @Deprecated public Bitmap getImageAsset(String id) { ImageAssetManager bm = getImageAssetManager(); if (bm != null) { @@ -1128,6 +1153,46 @@ public Bitmap getImageAsset(String id) { return null; } + /** + * 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. + * + * The returned bitmap could be from: + * * Embedded in the animation file as a base64 string. + * * In the same directory as the animation file. + * * In the same zip file as the animation file. + * * Returned from an {@link ImageAssetDelegate}. + * or null if the image doesn't exist from any of those places. + */ + @Nullable + public Bitmap getBitmapForId(String id) { + ImageAssetManager assetManager = getImageAssetManager(); + if (assetManager != null) { + return assetManager.bitmapForId(id); + } + return null; + } + + /** + * 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. + * + * The returned bitmap could be from: + * * Embedded in the animation file as a base64 string. + * * In the same directory as the animation file. + * * In the same zip file as the animation file. + * * Returned from an {@link ImageAssetDelegate}. + * or null if the image doesn't exist from any of those places. + */ + @Nullable + public LottieImageAsset getLottieImageAssetForId(String id) { + LottieComposition composition = this.composition; + if (composition == null) { + return null; + } + return composition.getImages().get(id); + } + private ImageAssetManager getImageAssetManager() { if (getCallback() == null) { // We can't get a bitmap since we can't get a Context from the callback. diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java index f1acbe66c..39606060a 100644 --- a/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java +++ b/lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java @@ -6,7 +6,7 @@ import androidx.annotation.RestrictTo; /** - * Data class describing an image asset exported by bodymovin. + * Data class describing an image asset embedded in a Lottie json file. */ public class LottieImageAsset { private final int width; @@ -36,6 +36,9 @@ public int getHeight() { return height; } + /** + * The reference id in the json file. + */ public String getId() { return id; } diff --git a/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java b/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java index d28061d8c..063435704 100644 --- a/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java +++ b/lottie/src/main/java/com/airbnb/lottie/manager/ImageAssetManager.java @@ -62,6 +62,10 @@ public void setDelegate(@Nullable ImageAssetDelegate assetDelegate) { return prevBitmap; } + @Nullable public LottieImageAsset getImageAssetById(String id) { + return imageAssets.get(id); + } + @Nullable public Bitmap bitmapForId(String id) { LottieImageAsset asset = imageAssets.get(id); if (asset == null) { diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java index 16534495f..6cd60b648 100644 --- a/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java +++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import com.airbnb.lottie.LottieDrawable; +import com.airbnb.lottie.LottieImageAsset; import com.airbnb.lottie.LottieProperty; import com.airbnb.lottie.animation.LPaint; import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation; @@ -24,16 +25,18 @@ public class ImageLayer extends BaseLayer { private final Paint paint = new LPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); private final Rect src = new Rect(); private final Rect dst = new Rect(); + @Nullable private final LottieImageAsset lottieImageAsset; @Nullable private BaseKeyframeAnimation colorFilterAnimation; @Nullable private BaseKeyframeAnimation imageAnimation; ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) { super(lottieDrawable, layerModel); + lottieImageAsset = lottieDrawable.getLottieImageAssetForId(layerModel.getRefId()); } @Override public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) { Bitmap bitmap = getBitmap(); - if (bitmap == null || bitmap.isRecycled()) { + if (bitmap == null || bitmap.isRecycled() || lottieImageAsset == null) { return; } float density = Utils.dpScale(); @@ -45,16 +48,21 @@ public class ImageLayer extends BaseLayer { canvas.save(); canvas.concat(parentMatrix); src.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); - dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density)); + if (lottieDrawable.getMaintainOriginalImageBounds()) { + dst.set(0, 0, (int) (lottieImageAsset.getWidth() * density), (int) (lottieImageAsset.getHeight() * density)); + } else { + dst.set(0, 0, (int) (bitmap.getWidth() * density), (int) (bitmap.getHeight() * density)); + } + canvas.drawBitmap(bitmap, src, dst, paint); canvas.restore(); } @Override public void getBounds(RectF outBounds, Matrix parentMatrix, boolean applyParents) { super.getBounds(outBounds, parentMatrix, applyParents); - Bitmap bitmap = getBitmap(); - if (bitmap != null) { - outBounds.set(0, 0, bitmap.getWidth() * Utils.dpScale(), bitmap.getHeight() * Utils.dpScale()); + if (lottieImageAsset != null) { + float scale = Utils.dpScale(); + outBounds.set(0, 0, lottieImageAsset.getWidth() * scale, lottieImageAsset.getHeight() * scale); boundsMatrix.mapRect(outBounds); } } @@ -63,11 +71,20 @@ public class ImageLayer extends BaseLayer { private Bitmap getBitmap() { if (imageAnimation != null) { Bitmap callbackBitmap = imageAnimation.getValue(); - if (callbackBitmap != null) + if (callbackBitmap != null) { return callbackBitmap; + } } String refId = layerModel.getRefId(); - return lottieDrawable.getImageAsset(refId); + Bitmap bitmapFromDrawable = lottieDrawable.getBitmapForId(refId); + if (bitmapFromDrawable != null) { + return bitmapFromDrawable; + } + LottieImageAsset asset = this.lottieImageAsset; + if (asset != null) { + return asset.getBitmap(); + } + return null; } @SuppressWarnings("SingleStatementInBlock") 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 b7e4e4be5..b05d0b0c9 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 @@ -9,10 +9,13 @@ import android.graphics.PorterDuff 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 import androidx.compose.runtime.Composable +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 @@ -26,13 +29,12 @@ 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.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext import kotlin.coroutines.resume -private val ActivityContentLock = Mutex() - /** * Set of properties that are available to all [SnapshotTestCase] runs. */ @@ -167,6 +169,12 @@ suspend fun SnapshotTestCaseContext.snapshotComposition( bitmapPool.release(bitmap) } +/** + * Use this to signal that the composition is not ready to be snapshot yet. + * This use useful if you are using things like `rememberLottieComposition` which parses a composition asynchronously. + */ +val LocalSnapshotReady = compositionLocalOf { MutableStateFlow(true) } + fun SnapshotTestCaseContext.loadCompositionFromAssetsSync(fileName: String): LottieComposition { return LottieCompositionFactory.fromAssetSync(context, fileName).value!! } @@ -180,53 +188,55 @@ suspend fun SnapshotTestCaseContext.snapshotComposable( log("Snapshotting $name") val composeView = ComposeView(context) composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT) - var bitmap = withContext(Dispatchers.Main) { - composeView.setContent { + val readyFlow = MutableStateFlow(null) + composeView.setContent { + CompositionLocalProvider(LocalSnapshotReady provides readyFlow) { content(RenderMode.SOFTWARE) } - 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) - } - } + if (readyFlow.value == null) readyFlow.value = true } onActivity { activity -> - activity.binding.content.removeView(composeView) + activity.binding.content.addView(composeView) } + readyFlow.first { it == true } + composeView.awaitFrame() + log("Drawing $name - Software") + var bitmap = bitmapPool.acquire(composeView.width, composeView.height) + var canvas = Canvas(bitmap) + composeView.draw(canvas) snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Software" else variant) bitmapPool.release(bitmap) if (renderHardwareAndSoftware) { - bitmap = withContext(Dispatchers.Main) { - composeView.setContent { + readyFlow.value = null + composeView.setContent { + CompositionLocalProvider(LocalSnapshotReady provides readyFlow) { 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) - } - } + if (readyFlow.value == null) readyFlow.value = true } - onActivity { activity -> - activity.binding.content.removeView(composeView) - } - snapshotter.record(bitmap, name, "$variant - Hardware") + readyFlow.first { it == true } + composeView.awaitFrame() + log("Drawing $name - Software") + bitmap = bitmapPool.acquire(composeView.width, composeView.height) + canvas = Canvas(bitmap) + composeView.draw(canvas) + snapshotter.record(bitmap, name, if (renderHardwareAndSoftware) "$variant - Hardware" else variant) bitmapPool.release(bitmap) } + + onActivity { activity -> + activity.binding.content.removeView(composeView) + } + LottieCompositionCache.getInstance().clear() +} + +private suspend fun View.awaitFrame() { + suspendCancellableCoroutine { cont -> + post { + cont.resume(Unit) + } + } } \ No newline at end of file diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeDynamicPropertiesTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeDynamicPropertiesTestCase.kt index 6356baf9d..a4b9be7ca 100644 --- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeDynamicPropertiesTestCase.kt +++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/ComposeDynamicPropertiesTestCase.kt @@ -1,21 +1,25 @@ package com.airbnb.lottie.snapshots.tests +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.Color -import androidx.compose.ui.platform.ComposeView +import androidx.compose.runtime.getValue import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.rememberLottieComposition import com.airbnb.lottie.compose.rememberLottieDynamicProperties import com.airbnb.lottie.compose.rememberLottieDynamicProperty +import com.airbnb.lottie.snapshots.LocalSnapshotReady import com.airbnb.lottie.snapshots.SnapshotTestCase import com.airbnb.lottie.snapshots.SnapshotTestCaseContext import com.airbnb.lottie.snapshots.snapshotComposable -import com.airbnb.lottie.snapshots.snapshotComposition class ComposeDynamicPropertiesTestCase : SnapshotTestCase { override suspend fun SnapshotTestCaseContext.run() { - val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/DynamicGradient.json").value!! snapshotComposable("Compose Dynamic Gradient") { + val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/DynamicGradient.json").value!! val dynamicProperties = rememberLottieDynamicProperties( rememberLottieDynamicProperty( LottieProperty.GRADIENT_COLOR, @@ -34,5 +38,48 @@ class ComposeDynamicPropertiesTestCase : SnapshotTestCase { ) LottieAnimation(composition, 0f, dynamicProperties = dynamicProperties) } + + val heartComposition = LottieCompositionFactory.fromAssetSync(context, "Tests/Heart.json").value!! + snapshotComposable("Compose Dynamic Image", "Default") { + val composition by rememberLottieComposition(LottieCompositionSpec.Asset("Tests/Heart.json")) + LocalSnapshotReady.current.value = composition != null + LottieAnimation(composition, 0f) + } + snapshotComposable("Compose Dynamic Image", "Default - Maintain Original Bounds") { + LottieAnimation(heartComposition, 0f, maintainOriginalImageBounds = true) + } + snapshotComposable("Compose Dynamic Image", "Smaller") { + val bitmap = getBitmapFromAssets("Images/Heart-80.png") + val dynamicProperties = rememberLottieDynamicProperties( + rememberLottieDynamicProperty(LottieProperty.IMAGE, bitmap, "Heart"), + ) + LottieAnimation(heartComposition, 0f, dynamicProperties = dynamicProperties) + } + snapshotComposable("Compose Dynamic Image", "Smaller - Maintain Original Bounds") { + val bitmap = getBitmapFromAssets("Images/Heart-80.png") + val dynamicProperties = rememberLottieDynamicProperties( + rememberLottieDynamicProperty(LottieProperty.IMAGE, bitmap, "Heart"), + ) + LottieAnimation(heartComposition, 0f, dynamicProperties = dynamicProperties, maintainOriginalImageBounds = true) + } + snapshotComposable("Compose Dynamic Image", "Larger") { + val bitmap = getBitmapFromAssets("Images/Heart-1200.png") + val dynamicProperties = rememberLottieDynamicProperties( + rememberLottieDynamicProperty(LottieProperty.IMAGE, bitmap, "Heart"), + ) + LottieAnimation(heartComposition, 0f, dynamicProperties = dynamicProperties) + } + snapshotComposable("Compose Dynamic Image", "Larger - Maintain Original Bounds") { + val bitmap = getBitmapFromAssets("Images/Heart-1200.png") + val dynamicProperties = rememberLottieDynamicProperties( + rememberLottieDynamicProperty(LottieProperty.IMAGE, bitmap, "Heart"), + ) + LottieAnimation(heartComposition, 0f, dynamicProperties = dynamicProperties, maintainOriginalImageBounds = true) + } + } + + private fun SnapshotTestCaseContext.getBitmapFromAssets(name: String): Bitmap { + @Suppress("BlockingMethodInNonBlockingContext") + return BitmapFactory.decodeStream(context.assets.open(name), null, BitmapFactory.Options())!! } } \ No newline at end of file diff --git a/snapshot-tests/src/main/assets/Images/Heart-1200.png b/snapshot-tests/src/main/assets/Images/Heart-1200.png new file mode 100644 index 000000000..27f52bf76 Binary files /dev/null and b/snapshot-tests/src/main/assets/Images/Heart-1200.png differ diff --git a/snapshot-tests/src/main/assets/Images/Heart-80.png b/snapshot-tests/src/main/assets/Images/Heart-80.png new file mode 100644 index 000000000..8f3eae4ab Binary files /dev/null and b/snapshot-tests/src/main/assets/Images/Heart-80.png differ diff --git a/snapshot-tests/src/main/assets/Tests/Heart.json b/snapshot-tests/src/main/assets/Tests/Heart.json new file mode 100644 index 000000000..43edfe5b6 --- /dev/null +++ b/snapshot-tests/src/main/assets/Tests/Heart.json @@ -0,0 +1 @@ +{"v":"5.8.1","fr":60,"ip":0,"op":32400,"w":400,"h":400,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":400,"h":400,"u":"","p":"data:image/png;base64,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","e":1}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Heart","refId":"image_0","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":[200,200,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":32400,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Blue Solid 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":[600,600,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":1200,"sh":1200,"sc":"#0600ff","ip":0,"op":32400,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/snapshot-tests/src/main/java/com/airbnb/lottie/snapshots/FilmStripView.kt b/snapshot-tests/src/main/java/com/airbnb/lottie/snapshots/FilmStripView.kt index 310a43c60..5b4ae8b80 100644 --- a/snapshot-tests/src/main/java/com/airbnb/lottie/snapshots/FilmStripView.kt +++ b/snapshot-tests/src/main/java/com/airbnb/lottie/snapshots/FilmStripView.kt @@ -33,6 +33,8 @@ class FilmStripView @JvmOverloads constructor( fun setImageAssetDelegate(delegate: ImageAssetDelegate?) { animationViews.forEach { it.setImageAssetDelegate(delegate) } + // Enable bitmap rescaling for the first 4 views so both APIs get test coverage. + animationViews.forEachIndexed { i, av -> av.maintainOriginalImageBounds = i <= 4 } } fun setFontAssetDelegate(delegate: FontAssetDelegate?) {