diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java index 37c83a5f1..53d05aee6 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 @@ private void init(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { 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 201c3bc28..1cf5d2053 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 { private final LottieFrameInfo frameInfo = new LottieFrameInfo<>(); @@ -31,6 +43,9 @@ public LottieValueCallback(@Nullable T staticValue) { * Override this if you haven't set a static value in the constructor or with setValue. *

* 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 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 0ef1f399c..879b65abf 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.OutlineMasksAndMattesTestCase 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 @@ class LottieSnapshotTest { 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 000000000..a858bd608 --- /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() { + override fun getValue(frameInfo: LottieFrameInfo?): 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