Merge remote-tracking branch 'origin/master' into gpeal/compose-dynamic-gradients
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 d6b8698..886747c 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
@@ -96,7 +96,6 @@
matrix.preTranslate(translation.x.toFloat(), translation.y.toFloat())
matrix.preScale(scale.scaleX, scale.scaleY)
-
drawable.composition = composition
if (dynamicProperties !== setDynamicProperties) {
setDynamicProperties?.removeFrom(drawable)
@@ -108,6 +107,7 @@
drawable.enableMergePathsForKitKatAndAbove(enableMergePaths)
drawable.useSoftwareRendering(useSoftwareRendering)
drawable.progress = progress
+ drawable.setBounds(0, 0, composition.bounds.width(), composition.bounds.height())
drawable.draw(canvas.nativeCanvas, matrix)
}
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
index fc7ee19..5882ef2 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieDrawable.java
@@ -14,6 +14,7 @@
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
@@ -62,7 +63,6 @@
void run(LottieComposition composition);
}
- private final Matrix matrix = new Matrix();
private LottieComposition composition;
private final LottieValueAnimator animator = new LottieValueAnimator();
private float scale = 1f;
@@ -106,12 +106,18 @@
private boolean outlineMasksAndMattes;
private boolean isApplyingOpacityToLayersEnabled;
+ 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 Paint softwareRenderingPaint;
- private final Rect softwareRenderingBoundsRect = new Rect();
+ private Rect softwareRenderingSrcBoundsRect;
+ private Rect softwareRenderingDstBoundsRect;
+ private RectF softwareRenderingDstBoundsRectF;
+ private RectF softwareRenderingTransformedBounds;
+ private Matrix softwareRenderingOriginalCanvasMatrix;
+ private Matrix softwareRenderingOriginalCanvasMatrixInverse;
/**
* True if the drawable has not been drawn since the last invalidateSelf.
@@ -223,7 +229,7 @@
return false;
}
- isDirty = false;
+ isDirty = true;
clearComposition();
this.composition = composition;
buildCompositionLayer();
@@ -1139,10 +1145,16 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public void draw(Canvas canvas, Matrix matrix) {
CompositionLayer compositionLayer = this.compositionLayer;
- if (compositionLayer == null) {
+ LottieComposition composition = this.composition;
+ if (compositionLayer == null || composition == null) {
return;
}
- compositionLayer.draw(canvas, matrix, alpha);
+
+ if (softwareRenderingEnabled) {
+ renderAndDrawAsBitmap(canvas, compositionLayer, matrix);
+ } else {
+ compositionLayer.draw(canvas, matrix, alpha);
+ }
}
private void drawWithNewAspectRatio(Canvas canvas) {
@@ -1152,35 +1164,34 @@
return;
}
- Rect bounds = getBounds();
- // 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();
-
- matrix.reset();
- matrix.preScale(scaleX, scaleY);
if (softwareRenderingEnabled) {
- renderAndDrawAsBitmap(canvas, compositionLayer, matrix);
+ renderAndDrawAsBitmap(canvas, compositionLayer, null);
} else {
- compositionLayer.draw(canvas, matrix, alpha);
+ Rect bounds = getBounds();
+ // 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;
}
- float scale = this.scale;
-
- matrix.reset();
- matrix.preScale(scale, scale);
if (softwareRenderingEnabled) {
- renderAndDrawAsBitmap(canvas, compositionLayer, matrix);
+ renderAndDrawAsBitmap(canvas, compositionLayer, null);
} else {
- compositionLayer.draw(canvas, matrix, alpha);
+ renderingMatrix.reset();
+ renderingMatrix.preScale(scale, scale);
+ compositionLayer.draw(canvas, renderingMatrix, alpha);
}
}
@@ -1191,33 +1202,95 @@
* @see LottieDrawable#useSoftwareRendering(boolean)
* @see LottieAnimationView#setRenderMode(RenderMode)
*/
- private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer, Matrix matrix) {
- if (softwareRenderingPaint == null) {
- softwareRenderingPaint = new LPaint();
+ private void renderAndDrawAsBitmap(Canvas originalCanvas, CompositionLayer compositionLayer, @Nullable Matrix parentMatrix) {
+ ensureSoftwareRenderingObjectsInitialized();
+
+ //noinspection deprecation
+ originalCanvas.getMatrix(softwareRenderingOriginalCanvasMatrix);
+ softwareRenderingOriginalCanvasMatrix.invert(softwareRenderingOriginalCanvasMatrixInverse);
+ renderingMatrix.set(softwareRenderingOriginalCanvasMatrix);
+ if (parentMatrix != null) {
+ renderingMatrix.postConcat(parentMatrix);
}
- int intrinsicWidth = getIntrinsicWidth();
- int intrinsicHeight = getIntrinsicHeight();
+ // Determine what bounds the animation will render to after taking into account the canvas and parent matrix.
+ softwareRenderingTransformedBounds.set(0f, 0f, getIntrinsicWidth(), getIntrinsicHeight());
+ renderingMatrix.mapRect(softwareRenderingTransformedBounds);
- if (softwareRenderingBitmap == null ||
- softwareRenderingBitmap.getWidth() < intrinsicWidth ||
- softwareRenderingBitmap.getHeight() < intrinsicHeight) {
- softwareRenderingBitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, Bitmap.Config.ARGB_8888);
- softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
- softwareRenderingClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
- softwareRenderingClearPaint.setColor(Color.BLACK);
+ // 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());
+
+ if (renderWidth == 0 || renderHeight == 0) {
+ return;
}
+ ensureSoftwareRenderingBitmap(renderWidth, renderHeight);
+
+ softwareRenderingSrcBoundsRect.set(0, 0, renderWidth, renderHeight);
+
if (isDirty) {
- if (softwareRenderingBitmap.getWidth() == intrinsicWidth && softwareRenderingBitmap.getHeight() == intrinsicHeight) {
- // eraseColor is ~10% faster than drawRect when covering the entire bitmap.
- softwareRenderingBitmap.eraseColor(0);
- } else {
- softwareRenderingCanvas.drawRect(0f, 0f, intrinsicWidth, intrinsicHeight, softwareRenderingClearPaint);
- }
- compositionLayer.draw(softwareRenderingCanvas, matrix, alpha);
- softwareRenderingBoundsRect.set(0, 0, intrinsicWidth, intrinsicHeight);
+ 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());
+ // 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);
+ compositionLayer.draw(softwareRenderingCanvas, renderingMatrix, alpha);
+
+ // Calculate the dst bounds.
+ // We need to map the rendered coordinates back to the canvas's coordinates. To do so, we need to invert the transform
+ // 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.
+ softwareRenderingOriginalCanvasMatrixInverse.mapRect(softwareRenderingDstBoundsRectF, softwareRenderingTransformedBounds);
+ convertRect(softwareRenderingDstBoundsRectF, softwareRenderingDstBoundsRect);
}
- originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingBoundsRect, softwareRenderingBoundsRect, softwareRenderingPaint);
+ originalCanvas.drawBitmap(softwareRenderingBitmap, softwareRenderingSrcBoundsRect, softwareRenderingDstBoundsRect, softwareRenderingPaint);
+ }
+
+ private void ensureSoftwareRenderingObjectsInitialized() {
+ if (softwareRenderingPaint != 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();
+ softwareRenderingTransformedBounds = new RectF();
+ softwareRenderingOriginalCanvasMatrix = new Matrix();
+ softwareRenderingOriginalCanvasMatrixInverse = new Matrix();
+ }
+
+ private void ensureSoftwareRenderingBitmap(int renderWidth, int renderHeight) {
+ if (softwareRenderingBitmap == null ||
+ softwareRenderingBitmap.getWidth() < renderWidth ||
+ softwareRenderingBitmap.getHeight() < renderHeight) {
+ softwareRenderingBitmap = Bitmap.createBitmap(renderWidth, renderHeight, Bitmap.Config.ARGB_8888);
+ softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
+ isDirty = true;
+ } else if (softwareRenderingBitmap.getWidth() > renderWidth || softwareRenderingBitmap.getHeight() > renderHeight) {
+ softwareRenderingBitmap = Bitmap.createBitmap(softwareRenderingBitmap, 0, 0, renderWidth, renderHeight);
+ softwareRenderingCanvas.setBitmap(softwareRenderingBitmap);
+ isDirty = true;
+ }
+ }
+
+ /**
+ * Convert a RectF to a Rect
+ */
+ private void convertRect(RectF src, Rect dst) {
+ dst.set(
+ (int) Math.floor(src.left),
+ (int) Math.floor(src.top),
+ (int) Math.ceil(src.right),
+ (int) Math.ceil(src.bottom)
+ );
}
}
diff --git a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
index 8c1f315..14b1c66 100644
--- a/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
+++ b/lottie/src/main/java/com/airbnb/lottie/model/layer/BaseLayer.java
@@ -77,12 +77,14 @@
private final Path path = new Path();
private final Matrix matrix = new Matrix();
+ private final Matrix canvasMatrix = new Matrix();
private final Paint contentPaint = new LPaint(Paint.ANTI_ALIAS_FLAG);
private final Paint dstInPaint = new LPaint(Paint.ANTI_ALIAS_FLAG, PorterDuff.Mode.DST_IN);
private final Paint dstOutPaint = new LPaint(Paint.ANTI_ALIAS_FLAG, PorterDuff.Mode.DST_OUT);
private final Paint mattePaint = new LPaint(Paint.ANTI_ALIAS_FLAG);
private final Paint clearPaint = new LPaint(PorterDuff.Mode.CLEAR);
private final RectF rect = new RectF();
+ private final RectF canvasBounds = new RectF();
private final RectF maskBoundsRect = new RectF();
private final RectF matteBoundsRect = new RectF();
private final RectF tempMaskBoundsRect = new RectF();
@@ -259,7 +261,17 @@
matrix.preConcat(transform.getMatrix());
intersectBoundsWithMask(rect, matrix);
- if (!rect.intersect(0, 0, canvas.getWidth(), canvas.getHeight())) {
+ // Intersect the mask and matte rect with the canvas bounds.
+ // If the canvas has a transform, then we need to transform its bounds by its matrix
+ // so that we know the coordinate space that the canvas is showing.
+ canvasBounds.set(0f, 0f, canvas.getWidth(), canvas.getHeight());
+ //noinspection deprecation
+ canvas.getMatrix(canvasMatrix);
+ if (!canvasMatrix.isIdentity()) {
+ canvasMatrix.invert(canvasMatrix);
+ canvasMatrix.mapRect(canvasBounds);
+ }
+ if (!rect.intersect(canvasBounds)) {
rect.set(0, 0, 0, 0);
}
diff --git a/snapshot-tests/build.gradle b/snapshot-tests/build.gradle
index 243105b..ee8ed56 100644
--- a/snapshot-tests/build.gradle
+++ b/snapshot-tests/build.gradle
@@ -65,6 +65,7 @@
implementation 'androidx.appcompat:appcompat:1.4.0-beta01'
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
+ implementation "androidx.compose.material:material:$composeVersion"
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
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 0acb9a0..6df7cd4 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,11 +17,11 @@
import com.airbnb.lottie.snapshots.tests.ApplyOpacityToLayerTestCase
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.CustomBoundsTestCase
import com.airbnb.lottie.snapshots.tests.DynamicPropertiesTestCase
import com.airbnb.lottie.snapshots.tests.FailureTestCase
import com.airbnb.lottie.snapshots.tests.FrameBoundariesTestCase
+import com.airbnb.lottie.snapshots.tests.LargeCompositionSoftwareRendering
import com.airbnb.lottie.snapshots.tests.MarkersTestCase
import com.airbnb.lottie.snapshots.tests.NightModeTestCase
import com.airbnb.lottie.snapshots.tests.OutlineMasksAndMattesTestCase
@@ -56,22 +56,21 @@
Manifest.permission.READ_EXTERNAL_STORAGE
)
+ lateinit var testCaseContext: SnapshotTestCaseContext
+ lateinit var snapshotter: HappoSnapshotter
+
@Before
fun setup() {
LottieCompositionCache.getInstance().resize(1)
- }
-
- @Test
- fun testAll() = runBlocking {
val context = ApplicationProvider.getApplicationContext<Context>()
- val snapshotter = HappoSnapshotter(context) { name, variant ->
+ snapshotter = HappoSnapshotter(context) { name, variant ->
snapshotActivityRule.scenario.onActivity { activity ->
activity.updateUiForSnapshot(name, variant)
}
}
- val testCaseContext: SnapshotTestCaseContext = object : SnapshotTestCaseContext {
+ testCaseContext = object : SnapshotTestCaseContext {
override val context: Context = context
- override val snapshotter: HappoSnapshotter = snapshotter
+ override val snapshotter: HappoSnapshotter = this@LottieSnapshotTest.snapshotter
override val bitmapPool: BitmapPool = BitmapPool()
override val animationViewPool: ObjectPool<LottieAnimationView> = ObjectPool {
val animationViewContainer = FrameLayout(context)
@@ -80,9 +79,7 @@
}
}
override val filmStripViewPool: ObjectPool<FilmStripView> = ObjectPool {
- FilmStripView(context).apply {
- setLayerType(View.LAYER_TYPE_NONE, null)
- }
+ FilmStripView(context)
}
override fun onActivity(callback: (SnapshotTestActivity) -> Unit) {
@@ -103,7 +100,10 @@
})
}
- val prodAnimations = ProdAnimationsTestCase()
+ }
+
+ @Test
+ fun testAll() = runBlocking {
val testCases = listOf(
CustomBoundsTestCase(),
ColorStateListColorFilterTestCase(),
@@ -118,13 +118,13 @@
NightModeTestCase(),
ApplyOpacityToLayerTestCase(),
OutlineMasksAndMattesTestCase(),
-// ComposeDynamicPropertiesTestCase(),
- prodAnimations,
+ LargeCompositionSoftwareRendering(),
+ ProdAnimationsTestCase(),
)
withTimeout(TimeUnit.MINUTES.toMillis(45)) {
launch {
- with(prodAnimations) {
+ with(testCases.filterIsInstance<ProdAnimationsTestCase>().firstOrNull() ?: return@launch) {
// Kick off the downloads ahead of time so it can start while the other tests are snapshotting
testCaseContext.downloadAnimations()
}
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 5e38ce0..f254de1 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,8 +1,11 @@
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
+import android.graphics.PorterDuff
import android.util.Log
import android.view.View
import android.view.ViewGroup
@@ -11,6 +14,9 @@
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
@@ -22,8 +28,10 @@
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
+import kotlin.coroutines.resume
private val ActivityContentLock = Mutex()
@@ -68,6 +76,8 @@
assetName: String,
snapshotName: String = assetName,
snapshotVariant: String = "default",
+ widthPx: Int = context.resources.displayMetrics.widthPixels,
+ heightPx: Int = context.resources.displayMetrics.heightPixels,
callback: (LottieAnimationView) -> Unit,
) {
val result = LottieCompositionFactory.fromAssetSync(context, assetName)
@@ -78,16 +88,15 @@
animationView.scale = 1f
animationView.scaleType = ImageView.ScaleType.FIT_CENTER
callback(animationView)
- val widthSpec = View.MeasureSpec.makeMeasureSpec(
- context.resources.displayMetrics
- .widthPixels,
- View.MeasureSpec.EXACTLY
- )
- val heightSpec = View.MeasureSpec.makeMeasureSpec(
- context.resources.displayMetrics
- .heightPixels, View.MeasureSpec.EXACTLY
- )
val animationViewContainer = animationView.parent as ViewGroup
+ val widthSpec = View.MeasureSpec.makeMeasureSpec(
+ widthPx,
+ View.MeasureSpec.EXACTLY,
+ )
+ val heightSpec: Int = View.MeasureSpec.makeMeasureSpec(
+ heightPx,
+ View.MeasureSpec.EXACTLY,
+ )
animationViewContainer.measure(widthSpec, heightSpec)
animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
val bitmap = bitmapPool.acquire(animationView.width, animationView.height)
@@ -133,6 +142,7 @@
val bitmap = bitmapPool.acquire(filmStripView.width, filmStripView.height)
val canvas = Canvas(bitmap)
filmStripView.setComposition(composition, name)
+ canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
withContext(Dispatchers.Main) {
log("Drawing $name")
filmStripView.draw(canvas)
@@ -150,17 +160,21 @@
) = withContext(Dispatchers.Default) {
log("Snapshotting $name")
val composeView = ComposeView(context)
- composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
- onActivity { activity ->
- activity.binding.content.addView(composeView)
- }
- awaitFrame()
- composeView.setContent(content)
- val bitmap = bitmapPool.acquire(composeView.width, composeView.height)
- val canvas = Canvas(bitmap)
- withContext(Dispatchers.Main) {
- log("Drawing $name")
- composeView.draw(canvas)
+ composeView.layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
+ val bitmap = withContext(Dispatchers.Main) {
+ composeView.setContent(content)
+ suspendCancellableCoroutine<Bitmap> { cont ->
+ composeView.doOnLayout {
+ log("Drawing $name")
+ val bitmap = bitmapPool.acquire(composeView.width, composeView.height)
+ val canvas = Canvas(bitmap)
+ composeView.draw(canvas)
+ cont.resume(bitmap)
+ }
+ onActivity { activity ->
+ activity.binding.content.addView(composeView)
+ }
+ }
}
onActivity { activity ->
activity.binding.content.removeView(composeView)
@@ -168,4 +182,4 @@
LottieCompositionCache.getInstance().clear()
snapshotter.record(bitmap, name, variant)
bitmapPool.release(bitmap)
-}
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..cdce72f
--- /dev/null
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/LargeCompositionSoftwareRendering.kt
@@ -0,0 +1,106 @@
+package com.airbnb.lottie.snapshots.tests
+
+import android.graphics.Matrix
+import android.widget.ImageView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieComposition
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.snapshots.SnapshotTestCase
+import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
+import com.airbnb.lottie.snapshots.snapshotComposable
+import com.airbnb.lottie.snapshots.withAnimationView
+
+class LargeCompositionSoftwareRendering : SnapshotTestCase {
+ override suspend fun SnapshotTestCaseContext.run() {
+ snapshotWithImageView("Default") {}
+ snapshotWithImageView("CenterCrop") { av ->
+ av.scaleType = ImageView.ScaleType.CENTER_CROP
+ }
+ 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
+ }
+ snapshotWithImageView("FitCenter") { av ->
+ av.scaleType = ImageView.ScaleType.FIT_CENTER
+ }
+ snapshotWithImageView("FitStart") { av ->
+ av.scaleType = ImageView.ScaleType.FIT_START
+ }
+ snapshotWithImageView("FitEnd") { av ->
+ av.scaleType = ImageView.ScaleType.FIT_END
+ }
+ snapshotWithImageView("FitXY") { av ->
+ av.scaleType = ImageView.ScaleType.FIT_XY
+ }
+ snapshotWithImageView("Matrix With Sew") { av ->
+ av.scaleType = ImageView.ScaleType.MATRIX
+ av.imageMatrix = Matrix().apply {
+ preScale(0.025f, 0.025f)
+ preSkew(1f, 0f)
+ }
+ }
+
+ snapshotWithComposable("Fit") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.Fit)
+ }
+ snapshotWithComposable("Crop") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.Crop)
+ }
+ snapshotWithComposable("FillBounds") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.FillBounds)
+ }
+ snapshotWithComposable("FillWidth") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.FillWidth)
+ }
+ snapshotWithComposable("FillHeight") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.FillHeight)
+ }
+ snapshotWithComposable("Inside") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.Inside)
+ }
+ snapshotWithComposable("None") { comp ->
+ LottieAnimation(comp, progress = 0f, contentScale = ContentScale.None)
+ }
+ }
+
+ private suspend fun SnapshotTestCaseContext.snapshotWithImageView(snapshotVariant: String, callback: (LottieAnimationView) -> Unit) {
+ withAnimationView("Tests/LargeComposition.json", "Large Composition Tests", snapshotVariant, widthPx = 275, heightPx = 275) { av ->
+ av.setBackgroundColor(0x7f7f7f7f)
+ callback(av)
+ }
+ }
+
+ private suspend fun SnapshotTestCaseContext.snapshotWithComposable(
+ snapshotVariant: String,
+ callback: @Composable (composition: LottieComposition) -> Unit
+ ) {
+ val composition = LottieCompositionFactory.fromAssetSync(context, "Tests/LargeComposition.json").value!!
+ snapshotComposable("Large Composition Tests - Compose", snapshotVariant) {
+ Box(
+ modifier = Modifier
+ .size(100.dp)
+ .background(Color.Gray)
+ ) {
+ callback(composition)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/BitmapPool.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/BitmapPool.kt
index 153687a..aae3c24 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/BitmapPool.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/BitmapPool.kt
@@ -15,17 +15,13 @@
private val semaphore = SuspendingSemaphore(MAX_RELEASED_BITMAPS)
private val bitmaps = Collections.synchronizedList(ArrayList<Bitmap>())
private val releasedBitmaps = ConcurrentHashMap<Bitmap, Bitmap>()
- private val clearPaint by lazy {
- Paint().apply {
- xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
- }
- }
+ @Synchronized
fun clear() {
bitmaps.clear()
}
- @ExperimentalCoroutinesApi
+ @Synchronized
fun acquire(width: Int, height: Int): Bitmap {
if (width <= 0 || height <= 0) {
return TRANSPARENT_1X1_BITMAP
@@ -45,25 +41,22 @@
bitmaps
.firstOrNull { it.width >= width && it.height >= height }
?.also { bitmaps.remove(it) }
- ?.also { it.eraseColor(0) }
} ?: createNewBitmap(width, height)
val croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height)
releasedBitmaps[croppedBitmap] = bitmap
- Canvas(croppedBitmap).apply {
- drawRect(0f, 0f, croppedBitmap.width.toFloat(), croppedBitmap.height.toFloat(), clearPaint)
- }
-
return croppedBitmap
}
+ @Synchronized
fun release(bitmap: Bitmap) {
if (bitmap == TRANSPARENT_1X1_BITMAP) {
return
}
val originalBitmap = releasedBitmaps.remove(bitmap) ?: throw IllegalArgumentException("Unable to find original bitmap.")
+ originalBitmap.eraseColor(0)
bitmaps += originalBitmap
semaphore.release()
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/ObjectPool.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/ObjectPool.kt
index b4c430a..16f8f36 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/ObjectPool.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/ObjectPool.kt
@@ -12,7 +12,6 @@
private val objects = Collections.synchronizedList(ArrayList<T>())
private val releasedObjects = HashSet<T>()
- @ExperimentalCoroutinesApi
@Synchronized
fun acquire(): T {
val blockedStartTime = System.currentTimeMillis()
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/Utils.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/Utils.kt
index d2e1baf..5501510 100644
--- a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/Utils.kt
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/utils/Utils.kt
@@ -7,27 +7,31 @@
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
-suspend fun TransferObserver.await() = suspendCoroutine<TransferObserver> { continuation ->
+suspend fun TransferObserver.await() = suspendCancellableCoroutine<TransferObserver> { continuation ->
val listener = object : TransferListener {
override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {}
override fun onError(id: Int, ex: Exception) {
Log.e(L.TAG, "$id failed uploading!", ex)
continuation.resumeWithException(ex)
+ cleanTransferListener()
}
override fun onStateChanged(id: Int, state: TransferState) {
when (state) {
TransferState.COMPLETED -> {
continuation.resume(this@await)
+ cleanTransferListener()
}
TransferState.CANCELED, TransferState.FAILED -> {
Log.d(L.TAG, "$id failed uploading ($state).")
continuation.resume(this@await)
+ cleanTransferListener()
}
else -> Unit
}
diff --git a/snapshot-tests/src/main/assets/Tests/LargeComposition.json b/snapshot-tests/src/main/assets/Tests/LargeComposition.json
new file mode 100644
index 0000000..6c07daa
--- /dev/null
+++ b/snapshot-tests/src/main/assets/Tests/LargeComposition.json
@@ -0,0 +1 @@
+{"v":"5.8.1","fr":25,"ip":0,"op":2,"w":2000,"h":4000,"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":[1000,2000,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":[{"ty":"rc","d":1,"s":{"a":0,"k":[1554.562,597.062],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.220496237278,1,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":[1.281,-21.469],"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":"Rectangle 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[1780.188,3801.75],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.116727701823,1,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":[2.094,12.875],"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":"Rectangle 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":2,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":1,"nm":"Red Solid 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1000,2000,0],"ix":2,"l":2},"a":{"a":0,"k":[1000,2000,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"sw":2000,"sh":4000,"sc":"#ff0000","ip":0,"op":2,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file
diff --git a/snapshot-tests/src/main/res/layout/film_strip_view.xml b/snapshot-tests/src/main/res/layout/film_strip_view.xml
index 1705fbf..26ebc8e 100644
--- a/snapshot-tests/src/main/res/layout/film_strip_view.xml
+++ b/snapshot-tests/src/main/res/layout/film_strip_view.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/grid_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -9,45 +10,54 @@
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_1"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_2"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_3"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_4"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_5"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_6"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="software" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_7"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="hardware" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_8"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="hardware" />
<com.airbnb.lottie.snapshots.NoCacheLottieAnimationView
android:id="@+id/animation_9"
android:layout_width="@dimen/film_strip_size"
- android:layout_height="@dimen/film_strip_size" />
+ android:layout_height="@dimen/film_strip_size"
+ app:lottie_renderMode="hardware" />
</GridLayout>
diff --git a/snapshot-tests/src/main/res/layout/snapshot_tests_activity.xml b/snapshot-tests/src/main/res/layout/snapshot_tests_activity.xml
index 05bdd73..93421bf 100644
--- a/snapshot-tests/src/main/res/layout/snapshot_tests_activity.xml
+++ b/snapshot-tests/src/main/res/layout/snapshot_tests_activity.xml
@@ -3,17 +3,14 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:padding="24dp">
- <TextView
- android:id="@+id/statusTextView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingEnd="24dp"
- android:paddingStart="24dp"
- android:paddingTop="24dp"
- android:textSize="10sp"
- tools:text="com.airbnb.android" />
+ <FrameLayout
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" />
<TextView
android:id="@+id/counterTextView"
@@ -25,9 +22,13 @@
android:text="0"
android:textSize="10sp" />
- <FrameLayout
- android:id="@+id/content"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1" />
+ <TextView
+ android:id="@+id/statusTextView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="24dp"
+ android:paddingStart="24dp"
+ android:paddingTop="24dp"
+ android:textSize="10sp"
+ tools:text="com.airbnb.android" />
</LinearLayout>
\ No newline at end of file