Skip to content

Commit

Permalink
[Compose] Allow custom cache keys, dynamic properties for images, and…
Browse files Browse the repository at this point in the history
… remove font remapping (#1847)

This set of changes is all around composition caching:

1) Increase the caching flexibility by allowing arbitrary cache keys.
2) Remove the cacheKey parameter from LottieCompositionSpec.JsonString because it was ambiguous with the new cacheKey parameter.
3) Add dynamic properties for bitmaps. This is helpful because it allows you to set a bitmap on a single LottieAnimation without overwriting the bitmap for the cacheable LottieComposition.
4) Removed fontRemapping for rememberLottieComposition because there was no way to know how to handle caching. Instead, dynamic properties can be used.
  • Loading branch information
gpeal authored Jul 17, 2021
1 parent e2aaf91 commit 194d96d
Show file tree
Hide file tree
Showing 18 changed files with 207 additions and 74 deletions.
1 change: 1 addition & 0 deletions .idea/inspectionProfiles/Project_Default.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package com.airbnb.lottie.compose
* passed into [rememberLottieComposition] or [LottieAnimation].
*/
sealed class LottieCompositionSpec {

/**
* Load an animation from res/raw.
*/
Expand Down Expand Up @@ -39,5 +40,5 @@ sealed class LottieCompositionSpec {
/**
* Load an animation from its json string.
*/
data class JsonString(val jsonString: String, val cacheKey: String? = null) : LottieCompositionSpec()
data class JsonString(val jsonString: String) : LottieCompositionSpec()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.airbnb.lottie.compose

object LottieConstants {
/**
* Use this with [animateLottieComposition#iterations] to repeat forever.
* Use this with [animateLottieCompositionAsState]'s iterations parameter to repeat forever.
*/
const val IterateForever = Integer.MAX_VALUE
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.airbnb.lottie.compose

import android.graphics.Bitmap
import android.graphics.ColorFilter
import android.graphics.PointF
import android.graphics.Typeface
Expand Down Expand Up @@ -97,6 +98,7 @@ class LottieDynamicProperties internal constructor(
private val colorFilterProperties: List<LottieDynamicProperty<ColorFilter>>,
private val intArrayProperties: List<LottieDynamicProperty<IntArray>>,
private val typefaceProperties: List<LottieDynamicProperty<Typeface>>,
private val bitmapProperties: List<LottieDynamicProperty<Bitmap>>,
) {
@Suppress("UNCHECKED_CAST")
constructor(properties: List<LottieDynamicProperty<*>>) : this(
Expand All @@ -107,6 +109,7 @@ class LottieDynamicProperties internal constructor(
properties.filter { it.property is ColorFilter } as List<LottieDynamicProperty<ColorFilter>>,
properties.filter { it.property is IntArray } as List<LottieDynamicProperty<IntArray>>,
properties.filter { it.property is Typeface } as List<LottieDynamicProperty<Typeface>>,
properties.filter { it.property is Bitmap } as List<LottieDynamicProperty<Bitmap>>,
)

internal fun addTo(drawable: LottieDrawable) {
Expand All @@ -131,6 +134,10 @@ class LottieDynamicProperties internal constructor(
typefaceProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
bitmapProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}

}

internal fun removeFrom(drawable: LottieDrawable) {
Expand All @@ -155,6 +162,9 @@ class LottieDynamicProperties internal constructor(
typefaceProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Typeface>?)
}
bitmapProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, null as LottieValueCallback<Bitmap>?)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ 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
* cache key for the composition.
*/
private const val DefaultCacheKey = "__LottieInternalDefaultCacheKey__"

/**
* Takes a [LottieCompositionSpec], attempts to load and parse the animation, and returns a [LottieCompositionResult].
*
Expand Down Expand Up @@ -54,11 +60,10 @@ import kotlin.coroutines.resumeWithException
* and should be set via fontRemapping or via dynamic properties.
* @param fontFileExtension The default file extension for font files specified in the fontAssetsFolder or fontRemapping.
* Defaults to ttf.
* @param fontRemapping Remaps family names as specified in the Lottie json file to font files stored in the fontAssetsFolder.
* This will automatically add the fontFileExtension so you should not include the font file extension
* in your remapping.
* @param cacheComposition Whether or not to cache the composition. If set to true, the next time an composition with this
* spec is fetched, it will return the existing one instead of parsing it again.
* @param cacheKey Set a cache key for this composition. When set, subsequent calls to fetch this composition will
* return directly from the cache instead of having to reload and parse the animation. Set this to
* null to skip the cache. By default, this will automatically generate a cache key derived
* from your [LottieCompositionSpec].
* @param onRetry An optional callback that will be called if loading the animation fails.
* It is passed the failed count (the number of times it has failed) and the exception
* from the previous attempt to load the composition. [onRetry] is a suspending function
Expand All @@ -71,8 +76,7 @@ fun rememberLottieComposition(
imageAssetsFolder: String? = null,
fontAssetsFolder: String = "fonts/",
fontFileExtension: String = ".ttf",
fontRemapping: Map<String, String> = emptyMap(),
cacheComposition: Boolean = true,
cacheKey: String? = DefaultCacheKey,
onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
): LottieCompositionResult {
val context = LocalContext.current
Expand All @@ -88,8 +92,7 @@ fun rememberLottieComposition(
imageAssetsFolder.ensureTrailingSlash(),
fontAssetsFolder.ensureTrailingSlash(),
fontFileExtension.ensureLeadingPeriod(),
fontRemapping,
cacheComposition,
cacheKey,
)
result.complete(composition)
} catch (e: Throwable) {
Expand All @@ -110,22 +113,21 @@ private suspend fun lottieComposition(
imageAssetsFolder: String?,
fontAssetsFolder: String?,
fontFileExtension: String,
fontRemapping: Map<String, String>,
cacheComposition: Boolean,
cacheKey: String?,
): LottieComposition {
val task = when (spec) {
is LottieCompositionSpec.RawRes -> {
if (cacheComposition) {
if (cacheKey == DefaultCacheKey) {
LottieCompositionFactory.fromRawRes(context, spec.resId)
} else {
LottieCompositionFactory.fromRawRes(context, spec.resId, null)
LottieCompositionFactory.fromRawRes(context, spec.resId, cacheKey)
}
}
is LottieCompositionSpec.Url -> {
if (cacheComposition) {
if (cacheKey == DefaultCacheKey) {
LottieCompositionFactory.fromUrl(context, spec.url)
} else {
LottieCompositionFactory.fromUrl(context, spec.url, null)
LottieCompositionFactory.fromUrl(context, spec.url, cacheKey)
}
}
is LottieCompositionSpec.File -> {
Expand All @@ -136,26 +138,27 @@ private suspend fun lottieComposition(
when {
spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
ZipInputStream(fis),
spec.fileName.takeIf { cacheComposition },
spec.fileName.takeIf { cacheKey != null },
)
else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheComposition })
else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheKey != null })
}
}
is LottieCompositionSpec.Asset -> {
if (cacheComposition) {
if (cacheKey == DefaultCacheKey) {
LottieCompositionFactory.fromAsset(context, spec.assetName)
} else {
LottieCompositionFactory.fromAsset(context, spec.assetName, null)
}
}
is LottieCompositionSpec.JsonString -> {
LottieCompositionFactory.fromJsonString(spec.jsonString, spec.cacheKey.takeIf { cacheComposition })
val jsonStringCacheKey = if (cacheKey == DefaultCacheKey) spec.jsonString.hashCode().toString() else cacheKey
LottieCompositionFactory.fromJsonString(spec.jsonString, jsonStringCacheKey)
}
}

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

Expand Down Expand Up @@ -230,12 +233,11 @@ private suspend fun loadFontsFromAssets(
composition: LottieComposition,
fontAssetsFolder: String?,
fontFileExtension: String,
fontRemapping: Map<String, String>,
) {
if (composition.fonts.isEmpty()) return
withContext(Dispatchers.IO) {
for (font in composition.fonts.values) {
maybeLoadTypefaceFromAssets(context, font, fontAssetsFolder, fontFileExtension, fontRemapping[font.family])
maybeLoadTypefaceFromAssets(context, font, fontAssetsFolder, fontFileExtension)
}
}
}
Expand All @@ -245,9 +247,8 @@ private fun maybeLoadTypefaceFromAssets(
font: Font,
fontAssetsFolder: String?,
fontFileExtension: String,
remappedFontPath: String?,
) {
val path = remappedFontPath ?: "$fontAssetsFolder${font.family}${fontFileExtension}"
val path = "$fontAssetsFolder${font.family}${fontFileExtension}"
val typefaceWithDefaultStyle = try {
Typeface.createFromAsset(context.assets, path)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

/**
* After Effects/Bodymovin composition model. This is the serialized model from which the
* animation will be created.
* animation will be created. It is designed to be stateless, cacheable, and shareable.
* <p>
* To create one, use {@link LottieCompositionFactory}.
* <p>
Expand Down
7 changes: 6 additions & 1 deletion lottie/src/main/java/com/airbnb/lottie/LottieImageAsset.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ public String getFileName() {
}

/**
* TODO
* Permanently sets the bitmap on this LottieImageAsset. This will:
* 1) Overwrite any existing Bitmaps.
* 2) Apply to *all* animations that use this LottieComposition.
*
* If you only want to replace the bitmap for this animation, use dynamic properties
* with {@link LottieProperty#IMAGE}.
*/
public void setBitmap(@Nullable Bitmap bitmap) {
this.bitmap = bitmap;
Expand Down
15 changes: 13 additions & 2 deletions lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.airbnb.lottie;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ColorFilter;
import android.graphics.PointF;
import android.graphics.Typeface;
Expand Down Expand Up @@ -163,8 +165,17 @@ public interface LottieProperty {
Float TEXT_SIZE = 14f;

ColorFilter COLOR_FILTER = new ColorFilter();

/**
* Array of ARGB colors that map to position stops in the original gradient.
* For example, a gradient from red to blue could be remapped with [0xFF00FF00, 0xFFFF00FF] (green to purple).
*/
Integer[] GRADIENT_COLOR = new Integer[0];

/**
* Set on text layers.
*/
Typeface TYPEFACE = Typeface.DEFAULT;
/**
* Set on image layers.
*/
Bitmap IMAGE = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
}
14 changes: 14 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/ImageLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class ImageLayer extends BaseLayer {
private final Rect src = new Rect();
private final Rect dst = new Rect();
@Nullable private BaseKeyframeAnimation<ColorFilter, ColorFilter> colorFilterAnimation;
@Nullable private BaseKeyframeAnimation<Bitmap, Bitmap> imageAnimation;

ImageLayer(LottieDrawable lottieDrawable, Layer layerModel) {
super(lottieDrawable, layerModel);
Expand Down Expand Up @@ -60,6 +61,11 @@ public class ImageLayer extends BaseLayer {

@Nullable
private Bitmap getBitmap() {
if (imageAnimation != null) {
Bitmap callbackBitmap = imageAnimation.getValue();
if (callbackBitmap != null)
return callbackBitmap;
}
String refId = layerModel.getRefId();
return lottieDrawable.getImageAsset(refId);
}
Expand All @@ -76,6 +82,14 @@ public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> ca
colorFilterAnimation =
new ValueCallbackKeyframeAnimation<>((LottieValueCallback<ColorFilter>) callback);
}
} else if (property == LottieProperty.IMAGE) {
if (callback == null) {
imageAnimation = null;
} else {
//noinspection unchecked
imageAnimation =
new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Bitmap>) callback);
}
}
}
}
Binary file added sample-compose/src/main/assets/Images/android.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.navigation.compose.rememberNavController
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.sample.compose.examples.AnimatableExamplesPage
import com.airbnb.lottie.sample.compose.examples.BasicUsageExamplesPage
import com.airbnb.lottie.sample.compose.examples.CachingExamplesPage
import com.airbnb.lottie.sample.compose.examples.ContentScaleExamplesPage
import com.airbnb.lottie.sample.compose.examples.DynamicPropertiesExamplesPage
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
Expand Down Expand Up @@ -104,6 +105,7 @@ class ComposeActivity : AppCompatActivity() {
composable(Route.ImagesExamples.route) { ImagesExamplesPage() }
composable(Route.TextExamples.route) { TextExamplesPage() }
composable(Route.ContentScaleExamples.route) { ContentScaleExamplesPage() }
composable(Route.CachingExamples.route) { CachingExamplesPage() }
composable(
Route.Player.fullRoute,
arguments = Route.Player.args
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyLi

object ContentScaleExamples : Route("ContentScale examples")

object CachingExamples : Route("Caching examples")

object Player : Route(
"player",
listOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.airbnb.lottie.sample.compose.examples

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.rememberLottieComposition
import com.airbnb.lottie.sample.compose.R

@Composable
fun CachingExamplesPage() {
UsageExamplePageScaffold {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
ExampleCard("Default Caching", "Lottie caches compositions by default") {
Example1()
}
ExampleCard("Day/Night", "Animations in raw/res will automatically respect day and night mode") {
Example2()
}
ExampleCard("Skip Cache", "Skip the cache") {
Example3()
}
}
}
}

@Composable
private fun Example1() {
// By default, Lottie will cache compositions with a key derived from your LottieCompositionSpec.
// If you request the composition multiple times or request it again at some point later, it
// will return the previous composition. LottieComposition itself it stateless. All stateful
// actions should happen within LottieAnimation.
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.heart))
LottieAnimation(composition)
}

@Composable
private fun Example2() {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.sun_moon))
LottieAnimation(composition)
}

@Composable
private fun Example3() {
val composition by rememberLottieComposition(
LottieCompositionSpec.RawRes(R.raw.we_accept_inline_image),
// Don't cache this composition. You may want to do this for animations that have images
// because the bitmaps are much larger to store than the rest of the animation.
cacheKey = null,
)
LottieAnimation(composition)
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,11 @@ fun ExamplesPage(navController: NavController) {
modifier = Modifier
.clickable { navController.navigate(Route.ContentScaleExamples) }
)
ListItem(
text = { Text("Caching") },
secondaryText = { Text("Interacting with Lottie's composition cache") },
modifier = Modifier
.clickable { navController.navigate(Route.CachingExamples) }
)
}
}
Loading

0 comments on commit 194d96d

Please sign in to comment.