Skip to content

Commit

Permalink
[Compose] Parse LottieComposition synchronously instead of using Lott…
Browse files Browse the repository at this point in the history
…ieTask (#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
  • Loading branch information
gpeal authored Sep 3, 2021
1 parent 749e349 commit c8c6997
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<LottieComposition> {
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<LottieComposition>(e)
}
}
}

val composition = task.await()
loadImagesFromAssets(context, composition, imageAssetsFolder)
loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension)
return composition
}

private suspend fun <T> LottieTask<T>.await(): T = suspendCancellableCoroutine { cont ->
addListener { c ->
if (!cont.isCompleted) cont.resume(c)
}.addFailureListener { e ->
if (!cont.isCompleted) cont.resumeWithException(e)
}
}

Expand Down

0 comments on commit c8c6997

Please sign in to comment.