Support Lottie to be rendered on per-window UI thread (#2685)
Issue: #2684
diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
index d9cd9ff..a4dfe40 100644
--- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
+++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/rememberLottieComposition.kt
@@ -186,16 +186,19 @@
val actualCacheKey = if (cacheKey == DefaultCacheKey) spec.fileName else cacheKey
when {
spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
+ context,
ZipInputStream(fis),
actualCacheKey,
)
spec.fileName.endsWith("tgs") -> LottieCompositionFactory.fromJsonInputStream(
+ context,
GZIPInputStream(fis),
actualCacheKey,
)
else -> LottieCompositionFactory.fromJsonInputStream(
+ context,
fis,
actualCacheKey,
)
@@ -213,7 +216,7 @@
is LottieCompositionSpec.JsonString -> {
val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
- LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey)
+ LottieCompositionFactory.fromJsonString(context, spec.jsonString, jsonStringCacheKey)
}
is LottieCompositionSpec.ContentProvider -> {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
index 8972e08..5e20771 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieAnimationView.java
@@ -492,7 +492,7 @@
private LottieTask<LottieComposition> fromRawRes(@RawRes final int rawRes) {
if (isInEditMode()) {
- return new LottieTask<>(() -> cacheComposition
+ return new LottieTask<>(getContext().getMainLooper(), () -> cacheComposition
? LottieCompositionFactory.fromRawResSync(getContext(), rawRes) : LottieCompositionFactory.fromRawResSync(getContext(), rawRes, null), true);
} else {
return cacheComposition ?
@@ -508,7 +508,7 @@
private LottieTask<LottieComposition> fromAssets(final String assetName) {
if (isInEditMode()) {
- return new LottieTask<>(() -> cacheComposition ?
+ return new LottieTask<>(getContext().getMainLooper(), () -> cacheComposition ?
LottieCompositionFactory.fromAssetSync(getContext(), assetName) : LottieCompositionFactory.fromAssetSync(getContext(), assetName, null), true);
} else {
return cacheComposition ?
@@ -546,7 +546,7 @@
* Auto-closes the stream.
*/
public void setAnimation(InputStream stream, @Nullable String cacheKey) {
- setCompositionTask(LottieCompositionFactory.fromJsonInputStream(stream, cacheKey));
+ setCompositionTask(LottieCompositionFactory.fromJsonInputStream(getContext(), stream, cacheKey));
}
/**
@@ -559,7 +559,7 @@
* Auto-closes the stream.
*/
public void setAnimation(ZipInputStream stream, @Nullable String cacheKey) {
- setCompositionTask(LottieCompositionFactory.fromZipStream(stream, cacheKey));
+ setCompositionTask(LottieCompositionFactory.fromZipStream(getContext(), stream, cacheKey));
}
/**
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
index 57b723c..4f1b353 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java
@@ -10,6 +10,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Typeface;
+import android.os.Looper;
import android.util.Base64;
import androidx.annotation.Nullable;
@@ -136,13 +137,19 @@
return fromUrl(context, url, "url_" + url);
}
+ public static LottieTask<LottieComposition> fromUrl(final Context context, final String url, @Nullable final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ return fromUrl(context, context.getMainLooper(), url, cacheKey);
+ }
+
/**
* Fetch an animation from an http url. Once it is downloaded once, Lottie will cache the file to disk for
* future use. Because of this, you may call `fromUrl` ahead of time to warm the cache if you think you
* might need an animation in the future.
*/
- public static LottieTask<LottieComposition> fromUrl(final Context context, final String url, @Nullable final String cacheKey) {
- return cache(cacheKey, () -> {
+ public static LottieTask<LottieComposition> fromUrl(
+ final Context context, final Looper uiLooper, final String url, @Nullable final String cacheKey) {
+ return cache(uiLooper, cacheKey, () -> {
LottieResult<LottieComposition> result = L.networkFetcher(context).fetchSync(context, url, cacheKey);
if (cacheKey != null && result.getValue() != null) {
LottieCompositionCache.getInstance().put(cacheKey, result.getValue());
@@ -187,13 +194,18 @@
* <p>
* To skip the cache, add null as a third parameter.
*
- * @see #fromZipStream(ZipInputStream, String)
+ * @see #fromZipStream(Context, ZipInputStream, String)
*/
public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName) {
String cacheKey = "asset_" + fileName;
return fromAsset(context, fileName, cacheKey);
}
+ public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName, final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ return fromAsset(context, context.getMainLooper(), fileName, cacheKey);
+ }
+
/**
* Parse an animation from src/main/assets. It is recommended to use {@link #fromRawRes(Context, int)} instead.
* The asset file name will be used as a cache key so future usages won't have to parse the json again.
@@ -201,12 +213,12 @@
* <p>
* Pass null as the cache key to skip the cache.
*
- * @see #fromZipStream(ZipInputStream, String)
+ * @see #fromZipStream(Context, ZipInputStream, String)
*/
- public static LottieTask<LottieComposition> fromAsset(Context context, final String fileName, @Nullable final String cacheKey) {
+ public static LottieTask<LottieComposition> fromAsset(Context context, Looper uiLooper, final String fileName, @Nullable final String cacheKey) {
// Prevent accidentally leaking an Activity.
final Context appContext = context.getApplicationContext();
- return cache(cacheKey, () -> fromAssetSync(appContext, fileName, cacheKey), null);
+ return cache(uiLooper, cacheKey, () -> fromAssetSync(appContext, fileName, cacheKey), null);
}
/**
@@ -254,9 +266,11 @@
* font family (fFamily) in your animation file.
*/
public static LottieTask<LottieComposition> fromInputStream(@Nullable Context context, InputStream inputStream, @Nullable String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ final Looper uiLooper = context != null ? context.getMainLooper() : Looper.getMainLooper();
// Prevent accidentally leaking an Activity.
final Context appContext = context == null ? null : context.getApplicationContext();
- return cache(cacheKey, () -> fromInputStreamSync(appContext, inputStream, cacheKey), null);
+ return cache(uiLooper, cacheKey, () -> fromInputStreamSync(appContext, inputStream, cacheKey), null);
}
/**
@@ -309,10 +323,12 @@
* Pass null as the cache key to skip caching.
*/
public static LottieTask<LottieComposition> fromRawRes(Context context, @RawRes final int rawRes, @Nullable final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ final Looper uiLooper = context.getMainLooper();
// Prevent accidentally leaking an Activity.
final WeakReference<Context> contextRef = new WeakReference<>(context);
final Context appContext = context.getApplicationContext();
- return cache(cacheKey, () -> {
+ return cache(uiLooper, cacheKey, () -> {
@Nullable Context originalContext = contextRef.get();
Context context1 = originalContext != null ? originalContext : appContext;
return fromRawResSync(context1, rawRes, cacheKey);
@@ -383,6 +399,11 @@
*
* @see #fromJsonInputStreamSync(InputStream, String, boolean)
*/
+ public static LottieTask<LottieComposition> fromJsonInputStream(final Context context, final InputStream stream, @Nullable final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ return cache(context.getMainLooper(), cacheKey, () -> fromJsonInputStreamSync(stream, cacheKey), () -> closeQuietly(stream));
+ }
+
public static LottieTask<LottieComposition> fromJsonInputStream(final InputStream stream, @Nullable final String cacheKey) {
return cache(cacheKey, () -> fromJsonInputStreamSync(stream, cacheKey), () -> closeQuietly(stream));
}
@@ -443,6 +464,11 @@
return cache(cacheKey, () -> fromJsonStringSync(json, cacheKey), null);
}
+ public static LottieTask<LottieComposition> fromJsonString(final Context context, final String json, @Nullable final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ return cache(context.getMainLooper(), cacheKey, () -> fromJsonStringSync(json, cacheKey), null);
+ }
+
/**
* Return a LottieComposition for the specified raw json string.
* If loading from a file, it is preferable to use the InputStream or rawRes version.
@@ -523,16 +549,21 @@
/**
* @see #fromZipStreamSync(Context, ZipInputStream, String)
*/
- public static LottieTask<LottieComposition> fromZipStream(Context context, final ZipInputStream inputStream, @Nullable final String cacheKey) {
- return cache(cacheKey, () -> fromZipStreamSync(context, inputStream, cacheKey), () -> closeQuietly(inputStream));
+ public static LottieTask<LottieComposition> fromZipStream(@Nullable Context context, final ZipInputStream inputStream,
+ @Nullable final String cacheKey) {
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ Looper uiLooper = context != null ? context.getMainLooper() : Looper.getMainLooper();
+ return cache(uiLooper, cacheKey, () -> fromZipStreamSync(context, inputStream, cacheKey), () -> closeQuietly(inputStream));
}
/**
* @see #fromZipStreamSync(Context, ZipInputStream, String)
*/
- public static LottieTask<LottieComposition> fromZipStream(Context context, final ZipInputStream inputStream,
+ public static LottieTask<LottieComposition> fromZipStream(@Nullable Context context, final ZipInputStream inputStream,
@Nullable final String cacheKey, boolean close) {
- return cache(cacheKey, () -> fromZipStreamSync(context, inputStream, cacheKey), close ? () -> closeQuietly(inputStream) : null);
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ Looper uiLooper = context != null ? context.getMainLooper() : Looper.getMainLooper();
+ return cache(uiLooper, cacheKey, () -> fromZipStreamSync(context, inputStream, cacheKey), close ? () -> closeQuietly(inputStream) : null);
}
/**
@@ -767,17 +798,22 @@
return null;
}
+ private static LottieTask<LottieComposition> cache(
+ @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable, @Nullable Runnable onCached) {
+ return cache(Looper.getMainLooper(), cacheKey, callable, onCached);
+ }
+
/**
* First, check to see if there are any in-progress tasks associated with the cache key and return it if there is.
* If not, create a new task for the callable.
* Then, add the new task to the task cache and set up listeners so it gets cleared when done.
*/
- private static LottieTask<LottieComposition> cache(@Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable,
- @Nullable Runnable onCached) {
+ private static LottieTask<LottieComposition> cache(
+ final Looper uiLooper, @Nullable final String cacheKey, Callable<LottieResult<LottieComposition>> callable, @Nullable Runnable onCached) {
LottieTask<LottieComposition> task = null;
final LottieComposition cachedComposition = cacheKey == null ? null : LottieCompositionCache.getInstance().get(cacheKey);
if (cachedComposition != null) {
- task = new LottieTask<>(cachedComposition);
+ task = new LottieTask<>(uiLooper, cachedComposition);
}
if (cacheKey != null && taskCache.containsKey(cacheKey)) {
task = taskCache.get(cacheKey);
@@ -789,7 +825,7 @@
return task;
}
- task = new LottieTask<>(callable);
+ task = new LottieTask<>(uiLooper, callable);
if (cacheKey != null) {
AtomicBoolean resultAlreadyCalled = new AtomicBoolean(false);
task.addListener(result -> {
diff --git a/lottie/src/main/java/com/airbnb/lottie/LottieTask.java b/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
index 30f9595..07b6d5a 100644
--- a/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
+++ b/lottie/src/main/java/com/airbnb/lottie/LottieTask.java
@@ -50,23 +50,35 @@
/* Preserve add order. */
private final Set<LottieListener<T>> successListeners = new LinkedHashSet<>(1);
private final Set<LottieListener<Throwable>> failureListeners = new LinkedHashSet<>(1);
- private final Handler handler = new Handler(Looper.getMainLooper());
+
+ /**
+ * Handler to notify listener on UI thread. If view is rendered on per-window UI thread, app should override
+ * {@link Context#getMainLooper()} for the {@link WindowContext} that hosts the view tree so that uiHandler
+ * will point to the per-window ui thread.
+ */
+ private final Handler uiHandler;
@Nullable private volatile LottieResult<T> result = null;
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public LottieTask(Callable<LottieResult<T>> runnable) {
- this(runnable, false);
+ public LottieTask(Looper uiLooper, Callable<LottieResult<T>> runnable) {
+ this(uiLooper, runnable, false);
}
public LottieTask(T result) {
+ this(Looper.getMainLooper(), result);
+ }
+
+ public LottieTask(Looper uiLooper, T result) {
+ uiHandler = new Handler(uiLooper);
setResult(new LottieResult<>(result));
}
/**
* runNow is only used for testing.
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Callable<LottieResult<T>> runnable, boolean runNow) {
+ @RestrictTo(RestrictTo.Scope.LIBRARY) LottieTask(Looper uiLooper, Callable<LottieResult<T>> runnable, boolean runNow) {
+ uiHandler = new Handler(uiLooper);
if (runNow) {
try {
setResult(runnable.call());
@@ -145,11 +157,11 @@
}
private void notifyListeners() {
- // Listeners should be called on the main thread.
- if (Looper.myLooper() == Looper.getMainLooper()) {
+ // Listeners should be called on the ui thread.
+ if (Looper.myLooper() == uiHandler.getLooper()) {
notifyListenersInternal();
} else {
- handler.post(this::notifyListenersInternal);
+ uiHandler.post(this::notifyListenersInternal);
}
}
diff --git a/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java b/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
index 0d1fda6..733a394 100644
--- a/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
+++ b/lottie/src/test/java/com/airbnb/lottie/LottieTaskTest.java
@@ -20,6 +20,8 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
+import android.os.Looper;
+
public class LottieTaskTest extends BaseTest {
@Mock
@@ -30,9 +32,11 @@
@Rule
public MockitoRule rule = MockitoJUnit.rule();
+ private final Looper uiLooper = Looper.getMainLooper();
+
@Test
public void testListener() {
- new LottieTask<>(() -> new LottieResult<>(5), true)
+ new LottieTask<>(uiLooper, () -> new LottieResult<>(5), true)
.addListener(successListener)
.addFailureListener(failureListener);
verify(successListener, times(1)).onResult(5);
@@ -42,7 +46,7 @@
@Test
public void testException() {
final IllegalStateException exception = new IllegalStateException("foo");
- new LottieTask<>((Callable<LottieResult<Integer>>) () -> {
+ new LottieTask<>(uiLooper, (Callable<LottieResult<Integer>>) () -> {
throw exception;
}, true)
.addListener(successListener)
@@ -58,18 +62,10 @@
@Test
public void testRemoveListener() {
final Semaphore lock = new Semaphore(0);
- LottieTask<Integer> task = new LottieTask<>(new Callable<LottieResult<Integer>>() {
- @Override public LottieResult<Integer> call() {
- return new LottieResult<>(5);
- }
- })
+ LottieTask<Integer> task = new LottieTask<>(uiLooper, () -> new LottieResult<>(5))
.addListener(successListener)
.addFailureListener(failureListener)
- .addListener(new LottieListener<Integer>() {
- @Override public void onResult(Integer result) {
- lock.release();
- }
- });
+ .addListener(result -> lock.release());
task.removeListener(successListener);
try {
lock.acquire();
@@ -82,11 +78,7 @@
@Test
public void testAddListenerAfter() {
- LottieTask<Integer> task = new LottieTask<>(new Callable<LottieResult<Integer>>() {
- @Override public LottieResult<Integer> call() {
- return new LottieResult<>(5);
- }
- }, true);
+ LottieTask<Integer> task = new LottieTask<>(uiLooper, () -> new LottieResult<>(5), true);
task.addListener(successListener);
task.addFailureListener(failureListener);
diff --git a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
index d68bb36..a30f99a 100644
--- a/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
+++ b/sample/src/main/kotlin/com/airbnb/lottie/samples/PlayerViewModel.kt
@@ -2,7 +2,9 @@
import android.animation.ValueAnimator
import android.app.Application
+import android.content.Context
import android.net.Uri
+import android.os.Looper
import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieTask
@@ -38,16 +40,17 @@
class PlayerViewModel(
initialState: PlayerState,
- private val application: Application
+ private val application: Application,
+ private val uiLooper: Looper,
) : MavericksViewModel<PlayerState>(initialState) {
fun fetchAnimation(args: CompositionArgs) {
val url = args.url
when {
- url != null -> LottieCompositionFactory.fromUrl(application, url, null)
+ url != null -> LottieCompositionFactory.fromUrl(application, uiLooper, url, null)
args.fileUri != null -> taskForUri(args.fileUri)
- args.asset != null -> LottieCompositionFactory.fromAsset(application, args.asset, null)
+ args.asset != null -> LottieCompositionFactory.fromAsset(application, uiLooper, args.asset, null)
else -> error("Don't know how to fetch animation for $args")
}
.addListener {
@@ -65,7 +68,7 @@
else -> error("Unknown scheme ${uri.scheme}")
}
- return LottieCompositionFactory.fromJsonInputStream(fis, null)
+ return LottieCompositionFactory.fromJsonInputStream(application, fis, null)
}
fun toggleRenderGraphVisible() = setState { copy(renderGraphVisible = !renderGraphVisible) }
@@ -114,7 +117,8 @@
companion object : MavericksViewModelFactory<PlayerViewModel, PlayerState> {
override fun create(viewModelContext: ViewModelContext, state: PlayerState): PlayerViewModel {
- return PlayerViewModel(state, viewModelContext.app())
+ // App can override {@link Context#getMainLooper()} to support lottie view on per-window ui thread.
+ return PlayerViewModel(state, viewModelContext.app(), viewModelContext.activity.mainLooper)
}
}
}