Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Compose] Allow custom cache keys, dynamic properties for images, and remove font remapping #1847

Merged
merged 4 commits into from
Jul 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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