Allow software rendering to be invalidated when dynamic properties change (#2034)
The test case failed to re-render before but works now.
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 37c83a5..53d05ae 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -236,6 +236,19 @@
super.unscheduleDrawable(who);
}
+ @Override public void invalidate() {
+ super.invalidate();
+ Drawable d = getDrawable();
+ if (d instanceof LottieDrawable && ((LottieDrawable) d).getRenderMode() == RenderMode.SOFTWARE) {
+ // This normally isn't needed. However, when using software rendering, Lottie caches rendered bitmaps
+ // and updates it when the animation changes internally.
+ // If you have dynamic properties with a value callback and want to update the value of the dynamic property, you need a way
+ // to tell Lottie that the bitmap is dirty and it needs to be re-rendered. Normal drawables always re-draw the actual shapes
+ // so this isn't an issue but for this path, we have to take the extra step of setting the dirty flag.
+ lottieDrawable.invalidateSelf();
+ }
+ }
+
@Override public void invalidateDrawable(@NonNull Drawable dr) {
if (getDrawable() == lottieDrawable) {
// We always want to invalidate the root drawable so it redraws the whole drawable.
diff --git a/lottie/src/main/java/com/airbnb/lottie/value/LottieValueCallback.java b/lottie/src/main/java/com/airbnb/lottie/value/LottieValueCallback.java
index 201c3bc..1cf5d20 100644
--- a/lottie/src/main/java/com/airbnb/lottie/value/LottieValueCallback.java
+++ b/lottie/src/main/java/com/airbnb/lottie/value/LottieValueCallback.java
@@ -4,11 +4,23 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
/**
* Allows you to set a callback on a resolved {@link com.airbnb.lottie.model.KeyPath} to modify
* its animation values at runtime.
+ *
+ * If your dynamic property does the following, you must call {@link LottieAnimationView#invalidate()} or
+ * {@link LottieDrawable#invalidateSelf()} each time you want to update this value.
+ * 1. Use {@link com.airbnb.lottie.RenderMode.SOFTWARE}
+ * 2. Rendering a static image (the animation is either paused or there are no values
+ * changing within the animation itself)
+ * When using software rendering, Lottie caches the internal rendering bitmap. Whenever the animation changes
+ * internally, Lottie knows to invalidate the bitmap and re-render it on the next frame. If the animation
+ * never changes but your dynamic property does outside of Lottie, Lottie must be notified that it changed
+ * in order to set the bitmap as dirty and re-render it on the next frame.
*/
public class LottieValueCallback<T> {
private final LottieFrameInfo<T> frameInfo = new LottieFrameInfo<>();
@@ -31,6 +43,9 @@
* Override this if you haven't set a static value in the constructor or with setValue.
* <p>
* Return null to resort to the default value.
+ *
+ * Refer to the javadoc for this class for a special case that requires manual invalidation
+ * each time you want to return something different from this method.
*/
@Nullable
public T getValue(LottieFrameInfo<T> frameInfo) {
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 0ef1f39..879b65a 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
@@ -30,6 +30,7 @@
import com.airbnb.lottie.snapshots.tests.PartialFrameProgressTestCase
import com.airbnb.lottie.snapshots.tests.ProdAnimationsTestCase
import com.airbnb.lottie.snapshots.tests.ScaleTypesTestCase
+import com.airbnb.lottie.snapshots.tests.SoftwareRenderingDynamicPropertiesInvalidationTestCase
import com.airbnb.lottie.snapshots.tests.TextTestCase
import com.airbnb.lottie.snapshots.utils.BitmapPool
import com.airbnb.lottie.snapshots.utils.HappoSnapshotter
@@ -125,6 +126,7 @@
ComposeDynamicPropertiesTestCase(),
ProdAnimationsTestCase(),
ClipChildrenTestCase(),
+ SoftwareRenderingDynamicPropertiesInvalidationTestCase(),
)
withTimeout(TimeUnit.MINUTES.toMillis(45)) {
diff --git a/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/SoftwareRenderingDynamicPropertiesInvalidationTestCase.kt b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/SoftwareRenderingDynamicPropertiesInvalidationTestCase.kt
new file mode 100644
index 0000000..a858bd6
--- /dev/null
+++ b/snapshot-tests/src/androidTest/java/com/airbnb/lottie/snapshots/tests/SoftwareRenderingDynamicPropertiesInvalidationTestCase.kt
@@ -0,0 +1,68 @@
+package com.airbnb.lottie.snapshots.tests
+
+import android.graphics.Canvas
+import android.graphics.Color
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import com.airbnb.lottie.LottieCompositionFactory
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.RenderMode
+import com.airbnb.lottie.model.KeyPath
+import com.airbnb.lottie.snapshots.R
+import com.airbnb.lottie.snapshots.SnapshotTestCase
+import com.airbnb.lottie.snapshots.SnapshotTestCaseContext
+import com.airbnb.lottie.value.LottieFrameInfo
+import com.airbnb.lottie.value.LottieValueCallback
+
+/**
+ * When using software rendering, Lottie caches its internal render bitmap if the animation changes.
+ * However, if a dynamic property changes in a LottieValueCallback, the consumer must call LottieAnimationView.invalidate()
+ * or LottieDrawable.invalidateSelf() to invalidate the drawing cache.
+ */
+class SoftwareRenderingDynamicPropertiesInvalidationTestCase : SnapshotTestCase {
+ override suspend fun SnapshotTestCaseContext.run() {
+ val animationView = animationViewPool.acquire()
+ val composition = LottieCompositionFactory.fromRawResSync(context, R.raw.heart).value!!
+ animationView.setComposition(composition)
+ animationView.renderMode = RenderMode.SOFTWARE
+ animationView.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
+ animationView.scaleType = ImageView.ScaleType.FIT_CENTER
+ 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
+ animationViewContainer.measure(widthSpec, heightSpec)
+ animationViewContainer.layout(0, 0, animationViewContainer.measuredWidth, animationViewContainer.measuredHeight)
+ val canvas = Canvas()
+
+ var color = Color.GREEN
+ animationView.addValueCallback(KeyPath("**", "Fill 1"), LottieProperty.COLOR, object : LottieValueCallback<Int>() {
+ override fun getValue(frameInfo: LottieFrameInfo<Int>?): Int {
+ return color
+ }
+ })
+
+ var bitmap = bitmapPool.acquire(animationView.width, animationView.height)
+ canvas.setBitmap(bitmap)
+ animationView.draw(canvas)
+ snapshotter.record(bitmap, "Heart Software Dynamic Property", "Green")
+ bitmapPool.release(bitmap)
+
+ bitmap = bitmapPool.acquire(animationView.width, animationView.height)
+ canvas.setBitmap(bitmap)
+ color = Color.BLUE
+ animationView.invalidate()
+ animationView.draw(canvas)
+ snapshotter.record(bitmap, "Heart Software Dynamic Property", "Blue")
+ bitmapPool.release(bitmap)
+
+ animationViewPool.release(animationView)
+ }
+}
\ No newline at end of file