Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow software rendering to be invalidated when dynamic properties change #2034

Merged
merged 1 commit into from Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
Expand Up @@ -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.
Expand Down
Expand Up @@ -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<>();
Expand All @@ -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.
* <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) {
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -125,6 +126,7 @@ class LottieSnapshotTest {
ComposeDynamicPropertiesTestCase(),
ProdAnimationsTestCase(),
ClipChildrenTestCase(),
SoftwareRenderingDynamicPropertiesInvalidationTestCase(),
)

withTimeout(TimeUnit.MINUTES.toMillis(45)) {
Expand Down
@@ -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)
}
}