From c8c699714cd62766e04d210d0ceabf40e2ba2b9a Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Fri, 3 Sep 2021 16:10:11 -0700 Subject: [PATCH] [Compose] Parse LottieComposition synchronously instead of using LottieTask (#1888) Using LottieTask under the hood incurred several extra thread hops including a main thread post. Switching it to a result like this is both faster and also enables custom factories to be used. From my initial tests, this cut the parse time for the heart animation in the repo roughly in half. Unfortunately, LaunchedTask takes a few ms to start. I tried with rememberCoroutineScope().launch and the initial delay was the same so I'm not sure if there is anything else that can be done here. Fixes #1880 --- .../lottie/compose/LottieCompositionSpec.kt | 7 ++ .../compose/rememberLottieComposition.kt | 65 ++++++++++--------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt index 9444a5b5ea..ba38a5b760 100644 --- a/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt +++ b/lottie-compose/src/main/java/com/airbnb/lottie/compose/LottieCompositionSpec.kt @@ -1,5 +1,7 @@ package com.airbnb.lottie.compose +import com.airbnb.lottie.LottieComposition + /** * Specification for a [com.airbnb.lottie.LottieComposition]. Each subclass represents a different source. * A [com.airbnb.lottie.LottieComposition] is the stateless parsed version of a Lottie json file and is @@ -41,4 +43,9 @@ sealed interface LottieCompositionSpec { * Load an animation from its json string. */ inline class JsonString(val jsonString: String) : LottieCompositionSpec + + /** + * Load an animation from a custom factory. This will be called on an IO thread pool. + */ + inline class Custom(val factory: () -> LottieComposition) : LottieCompositionSpec } 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 42d3e3c198..f4f4187eff 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 @@ -13,18 +13,15 @@ import androidx.compose.ui.platform.LocalContext import com.airbnb.lottie.LottieComposition import com.airbnb.lottie.LottieCompositionFactory import com.airbnb.lottie.LottieImageAsset -import com.airbnb.lottie.LottieTask +import com.airbnb.lottie.LottieResult import com.airbnb.lottie.model.Font import com.airbnb.lottie.utils.Logger import com.airbnb.lottie.utils.Utils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.io.FileInputStream import java.io.IOException import java.util.zip.ZipInputStream -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Use this with [rememberLottieComposition#cacheKey]'s cacheKey parameter to generate a default @@ -81,6 +78,7 @@ fun rememberLottieComposition( ): LottieCompositionResult { val context = LocalContext.current val result by remember(spec) { mutableStateOf(LottieCompositionResultImpl()) } + LaunchedEffect(spec) { var exception: Throwable? = null var failedCount = 0 @@ -115,58 +113,63 @@ private suspend fun lottieComposition( fontFileExtension: String, cacheKey: String?, ): LottieComposition { - val task = when (spec) { + val result = parseCompositionSync(context, spec, cacheKey) + result.exception?.let { throw it } + + val composition = result.value!! + loadImagesFromAssets(context, composition, imageAssetsFolder) + loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension) + return composition +} + +private fun parseCompositionSync( + context: Context, + spec: LottieCompositionSpec, + cacheKey: String?, +): LottieResult { + return when (spec) { is LottieCompositionSpec.RawRes -> { if (cacheKey == DefaultCacheKey) { - LottieCompositionFactory.fromRawRes(context, spec.resId) + LottieCompositionFactory.fromRawResSync(context, spec.resId) } else { - LottieCompositionFactory.fromRawRes(context, spec.resId, cacheKey) + LottieCompositionFactory.fromRawResSync(context, spec.resId, cacheKey) } } is LottieCompositionSpec.Url -> { if (cacheKey == DefaultCacheKey) { - LottieCompositionFactory.fromUrl(context, spec.url) + LottieCompositionFactory.fromUrlSync(context, spec.url) } else { - LottieCompositionFactory.fromUrl(context, spec.url, cacheKey) + LottieCompositionFactory.fromUrlSync(context, spec.url, cacheKey) } } is LottieCompositionSpec.File -> { - val fis = withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") - FileInputStream(spec.fileName) - } + val fis = FileInputStream(spec.fileName) when { - spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream( + spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStreamSync( ZipInputStream(fis), spec.fileName.takeIf { cacheKey != null }, ) - else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheKey != null }) + else -> LottieCompositionFactory.fromJsonInputStreamSync(fis, spec.fileName.takeIf { cacheKey != null }) } } is LottieCompositionSpec.Asset -> { if (cacheKey == DefaultCacheKey) { - LottieCompositionFactory.fromAsset(context, spec.assetName) + LottieCompositionFactory.fromAssetSync(context, spec.assetName) } else { - LottieCompositionFactory.fromAsset(context, spec.assetName, null) + LottieCompositionFactory.fromAssetSync(context, spec.assetName, null) } } is LottieCompositionSpec.JsonString -> { val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey - LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey) + LottieCompositionFactory.fromJsonStringSync(spec.jsonString, jsonStringCacheKey) + } + is LottieCompositionSpec.Custom -> { + try { + LottieResult(spec.factory()) + } catch (e: Throwable) { + LottieResult(e) + } } - } - - val composition = task.await() - loadImagesFromAssets(context, composition, imageAssetsFolder) - loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension) - return composition -} - -private suspend fun LottieTask.await(): T = suspendCancellableCoroutine { cont -> - addListener { c -> - if (!cont.isCompleted) cont.resume(c) - }.addFailureListener { e -> - if (!cont.isCompleted) cont.resumeWithException(e) } }