• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.airbnb.lottie.compose
2 
3 import android.content.Context
4 import android.graphics.BitmapFactory
5 import android.graphics.Typeface
6 import android.util.Base64
7 import androidx.compose.runtime.Composable
8 import androidx.compose.runtime.LaunchedEffect
9 import androidx.compose.runtime.getValue
10 import androidx.compose.runtime.mutableStateOf
11 import androidx.compose.runtime.remember
12 import androidx.compose.ui.platform.LocalContext
13 import com.airbnb.lottie.LottieComposition
14 import com.airbnb.lottie.LottieCompositionFactory
15 import com.airbnb.lottie.LottieImageAsset
16 import com.airbnb.lottie.LottieTask
17 import com.airbnb.lottie.model.Font
18 import com.airbnb.lottie.utils.Logger
19 import com.airbnb.lottie.utils.Utils
20 import kotlinx.coroutines.Dispatchers
21 import kotlinx.coroutines.suspendCancellableCoroutine
22 import kotlinx.coroutines.withContext
23 import java.io.FileInputStream
24 import java.io.IOException
25 import java.util.zip.GZIPInputStream
26 import java.util.zip.ZipInputStream
27 import kotlin.coroutines.resume
28 import kotlin.coroutines.resumeWithException
29 
30 /**
31  * Use this with [rememberLottieComposition#cacheKey]'s cacheKey parameter to generate a default
32  * cache key for the composition.
33  */
34 private const val DefaultCacheKey = "__LottieInternalDefaultCacheKey__"
35 
36 /**
37  * Takes a [LottieCompositionSpec], attempts to load and parse the animation, and returns a [LottieCompositionResult].
38  *
39  * [LottieCompositionResult] allows you to explicitly check for loading, failures, call
40  * [LottieCompositionResult.await], or invoke it like a function to get the nullable composition.
41  *
42  * [LottieCompositionResult] implements State<LottieComposition?> so if you don't need the full result class,
43  * you can use this function like:
44  * ```
45  * val compositionResult: LottieCompositionResult = lottieComposition(spec)
46  * // or...
47  * val composition: State<LottieComposition?> by lottieComposition(spec)
48  * ```
49  *
50  * The loaded composition will automatically load and set images that are embedded in the json as a base64 string
51  * or will load them from assets if an imageAssetsFolder is supplied.
52  *
53  * @param spec The [LottieCompositionSpec] that defines which LottieComposition should be loaded.
54  * @param imageAssetsFolder A subfolder in `src/main/assets` that contains the exported images
55  *                          that this composition uses. DO NOT rename any images from your design tool. The
56  *                          filenames must match the values that are in your json file.
57  * @param fontAssetsFolder The default folder Lottie will look in to find font files. Fonts will be matched
58  *                         based on the family name specified in the Lottie json file.
59  *                         Defaults to "fonts/" so if "Helvetica" was in the Json file, Lottie will auto-match
60  *                         fonts located at "src/main/assets/fonts/Helvetica.ttf". Missing fonts will be skipped
61  *                         and should be set via fontRemapping or via dynamic properties.
62  * @param fontFileExtension The default file extension for font files specified in the fontAssetsFolder or fontRemapping.
63  *                          Defaults to ttf.
64  * @param cacheKey Set a cache key for this composition. When set, subsequent calls to fetch this composition will
65  *                 return directly from the cache instead of having to reload and parse the animation. Set this to
66  *                 null to skip the cache. By default, this will automatically generate a cache key derived
67  *                 from your [LottieCompositionSpec].
68  * @param onRetry An optional callback that will be called if loading the animation fails.
69  *                It is passed the failed count (the number of times it has failed) and the exception
70  *                from the previous attempt to load the composition. [onRetry] is a suspending function
71  *                so you can do things like add a backoff delay or await an internet connection before
72  *                retrying again. [rememberLottieRetrySignal] can be used to handle explicit retires.
73  */
74 @Composable
75 @JvmOverloads
76 fun rememberLottieComposition(
77     spec: LottieCompositionSpec,
78     imageAssetsFolder: String? = null,
79     fontAssetsFolder: String = "fonts/",
80     fontFileExtension: String = ".ttf",
81     cacheKey: String? = DefaultCacheKey,
82     onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
83 ): LottieCompositionResult {
84     val context = LocalContext.current
<lambda>null85     val result by remember(spec) { mutableStateOf(LottieCompositionResultImpl()) }
86     // Warm the task cache. We can start the parsing task before the LaunchedEffect gets dispatched and run.
87     // The LaunchedEffect task will join the task created inline here via LottieCompositionFactory's task cache.
<lambda>null88     remember(spec, cacheKey) { lottieTask(context, spec, cacheKey, isWarmingCache = true) }
<lambda>null89     LaunchedEffect(spec, cacheKey) {
90         var exception: Throwable? = null
91         var failedCount = 0
92         while (!result.isSuccess && (failedCount == 0 || onRetry(failedCount, exception!!))) {
93             try {
94                 val composition = lottieComposition(
95                     context,
96                     spec,
97                     imageAssetsFolder.ensureTrailingSlash(),
98                     fontAssetsFolder.ensureTrailingSlash(),
99                     fontFileExtension.ensureLeadingPeriod(),
100                     cacheKey,
101                 )
102                 result.complete(composition)
103             } catch (e: Throwable) {
104                 exception = e
105                 failedCount++
106             }
107         }
108         if (!result.isComplete && exception != null) {
109             result.completeExceptionally(exception)
110         }
111     }
112     return result
113 }
114 
lottieCompositionnull115 private suspend fun lottieComposition(
116     context: Context,
117     spec: LottieCompositionSpec,
118     imageAssetsFolder: String?,
119     fontAssetsFolder: String?,
120     fontFileExtension: String,
121     cacheKey: String?,
122 ): LottieComposition {
123     val task = requireNotNull(lottieTask(context, spec, cacheKey, isWarmingCache = false)) {
124         "Unable to create parsing task for $spec."
125     }
126 
127     val composition = task.await()
128     loadImagesFromAssets(context, composition, imageAssetsFolder)
129     loadFontsFromAssets(context, composition, fontAssetsFolder, fontFileExtension)
130     return composition
131 }
132 
lottieTasknull133 private fun lottieTask(
134     context: Context,
135     spec: LottieCompositionSpec,
136     cacheKey: String?,
137     isWarmingCache: Boolean,
138 ): LottieTask<LottieComposition>? {
139     return when (spec) {
140         is LottieCompositionSpec.RawRes -> {
141             if (cacheKey == DefaultCacheKey) {
142                 LottieCompositionFactory.fromRawRes(context, spec.resId)
143             } else {
144                 LottieCompositionFactory.fromRawRes(context, spec.resId, cacheKey)
145             }
146         }
147         is LottieCompositionSpec.Url -> {
148             if (cacheKey == DefaultCacheKey) {
149                 LottieCompositionFactory.fromUrl(context, spec.url)
150             } else {
151                 LottieCompositionFactory.fromUrl(context, spec.url, cacheKey)
152             }
153         }
154         is LottieCompositionSpec.File -> {
155             if (isWarmingCache) {
156                 // Warming the cache is done from the main thread so we can't
157                 // create the FileInputStream needed in this path.
158                 null
159             } else {
160                 val fis = FileInputStream(spec.fileName)
161                 val actualCacheKey = if (cacheKey == DefaultCacheKey) spec.fileName else cacheKey
162                 when {
163                     spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
164                         ZipInputStream(fis),
165                         actualCacheKey,
166                     )
167                     spec.fileName.endsWith("tgs") -> LottieCompositionFactory.fromJsonInputStream(
168                         GZIPInputStream(fis),
169                         actualCacheKey,
170                     )
171                     else -> LottieCompositionFactory.fromJsonInputStream(
172                         fis,
173                         actualCacheKey,
174                     )
175                 }
176             }
177         }
178         is LottieCompositionSpec.Asset -> {
179             if (cacheKey == DefaultCacheKey) {
180                 LottieCompositionFactory.fromAsset(context, spec.assetName)
181             } else {
182                 LottieCompositionFactory.fromAsset(context, spec.assetName, cacheKey)
183             }
184         }
185         is LottieCompositionSpec.JsonString -> {
186             val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
187             LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey)
188         }
189         is LottieCompositionSpec.ContentProvider -> {
190             val fis = context.contentResolver.openInputStream(spec.uri)
191             val actualCacheKey = if (cacheKey == DefaultCacheKey) spec.uri.toString() else cacheKey
192             when {
193                 spec.uri.toString().endsWith("zip") -> LottieCompositionFactory.fromZipStream(
194                     ZipInputStream(fis),
195                     actualCacheKey,
196                 )
197                 spec.uri.toString().endsWith("tgs") -> LottieCompositionFactory.fromJsonInputStream(
198                     GZIPInputStream(fis),
199                     actualCacheKey,
200                 )
201                 else -> LottieCompositionFactory.fromJsonInputStream(
202                     fis,
203                     actualCacheKey,
204                 )
205             }
206         }
207     }
208 }
209 
awaitnull210 private suspend fun <T> LottieTask<T>.await(): T = suspendCancellableCoroutine { cont ->
211     addListener { c ->
212         if (!cont.isCompleted) cont.resume(c)
213     }.addFailureListener { e ->
214         if (!cont.isCompleted) cont.resumeWithException(e)
215     }
216 }
217 
loadImagesFromAssetsnull218 private suspend fun loadImagesFromAssets(
219     context: Context,
220     composition: LottieComposition,
221     imageAssetsFolder: String?,
222 ) {
223     if (!composition.hasImages()) {
224         return
225     }
226     withContext(Dispatchers.IO) {
227         for (asset in composition.images.values) {
228             maybeDecodeBase64Image(asset)
229             maybeLoadImageFromAsset(context, asset, imageAssetsFolder)
230         }
231     }
232 }
233 
maybeLoadImageFromAssetnull234 private fun maybeLoadImageFromAsset(
235     context: Context,
236     asset: LottieImageAsset,
237     imageAssetsFolder: String?,
238 ) {
239     if (asset.bitmap != null || imageAssetsFolder == null) return
240     val filename = asset.fileName
241     val inputStream = try {
242         context.assets.open(imageAssetsFolder + filename)
243     } catch (e: IOException) {
244         Logger.warning("Unable to open asset.", e)
245         return
246     }
247     try {
248         val opts = BitmapFactory.Options()
249         opts.inScaled = true
250         opts.inDensity = 160
251         var bitmap = BitmapFactory.decodeStream(inputStream, null, opts)
252         bitmap = Utils.resizeBitmapIfNeeded(bitmap, asset.width, asset.height)
253         asset.bitmap = bitmap
254     } catch (e: IllegalArgumentException) {
255         Logger.warning("Unable to decode image.", e)
256     }
257 }
258 
maybeDecodeBase64Imagenull259 private fun maybeDecodeBase64Image(asset: LottieImageAsset) {
260     if (asset.bitmap != null) return
261     val filename = asset.fileName
262     if (filename.startsWith("data:") && filename.indexOf("base64,") > 0) {
263         // Contents look like a base64 data URI, with the format data:image/png;base64,<data>.
264         try {
265             val data = Base64.decode(filename.substring(filename.indexOf(',') + 1), Base64.DEFAULT)
266             val opts = BitmapFactory.Options()
267             opts.inScaled = true
268             opts.inDensity = 160
269             asset.bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, opts)
270         } catch (e: IllegalArgumentException) {
271             Logger.warning("data URL did not have correct base64 format.", e)
272         }
273     }
274 }
275 
loadFontsFromAssetsnull276 private suspend fun loadFontsFromAssets(
277     context: Context,
278     composition: LottieComposition,
279     fontAssetsFolder: String?,
280     fontFileExtension: String,
281 ) {
282     if (composition.fonts.isEmpty()) return
283     withContext(Dispatchers.IO) {
284         for (font in composition.fonts.values) {
285             maybeLoadTypefaceFromAssets(context, font, fontAssetsFolder, fontFileExtension)
286         }
287     }
288 }
289 
maybeLoadTypefaceFromAssetsnull290 private fun maybeLoadTypefaceFromAssets(
291     context: Context,
292     font: Font,
293     fontAssetsFolder: String?,
294     fontFileExtension: String,
295 ) {
296     val path = "$fontAssetsFolder${font.family}${fontFileExtension}"
297     val typefaceWithDefaultStyle = try {
298         Typeface.createFromAsset(context.assets, path)
299     } catch (e: Exception) {
300         Logger.error("Failed to find typeface in assets with path $path.", e)
301         return
302     }
303     try {
304         val typefaceWithStyle = typefaceForStyle(typefaceWithDefaultStyle, font.style)
305         font.typeface = typefaceWithStyle
306     } catch (e: Exception) {
307         Logger.error("Failed to create ${font.family} typeface with style=${font.style}!", e)
308     }
309 }
310 
typefaceForStylenull311 private fun typefaceForStyle(typeface: Typeface, style: String): Typeface? {
312     val containsItalic = style.contains("Italic")
313     val containsBold = style.contains("Bold")
314     val styleInt = when {
315         containsItalic && containsBold -> Typeface.BOLD_ITALIC
316         containsItalic -> Typeface.ITALIC
317         containsBold -> Typeface.BOLD
318         else -> Typeface.NORMAL
319     }
320     return if (typeface.style == styleInt) typeface else Typeface.create(typeface, styleInt)
321 }
322 
ensureTrailingSlashnull323 private fun String?.ensureTrailingSlash(): String? = when {
324     isNullOrBlank() -> null
325     endsWith('/') -> this
326     else -> "$this/"
327 }
328 
Stringnull329 private fun String.ensureLeadingPeriod(): String = when {
330     isBlank() -> this
331     startsWith(".") -> this
332     else -> ".$this"
333 }
334