Skip to content

Commit

Permalink
[Compose] Allow setting and remapping fonts (#1842)
Browse files Browse the repository at this point in the history
This PR adds 4 APIs to load typefaces in lottie-compose:
1) Using default paths. Fonts are in /assets/fonts/FAMILY_NAME.ttf
2) Overriding the assets subfolder or font file extension
3) Remapping family names to font files in assets
4) Dynamic properties

The dynamic properties API can also be used with lottie-android.
  • Loading branch information
gpeal authored Jul 15, 2021
1 parent 79cc077 commit f6a7569
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.airbnb.lottie.compose

import android.graphics.ColorFilter
import android.graphics.PointF
import android.graphics.Typeface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -95,6 +96,7 @@ class LottieDynamicProperties internal constructor(
private val scaleProperties: List<LottieDynamicProperty<ScaleXY>>,
private val colorFilterProperties: List<LottieDynamicProperty<ColorFilter>>,
private val intArrayProperties: List<LottieDynamicProperty<IntArray>>,
private val typefaceProperties: List<LottieDynamicProperty<Typeface>>,
) {
@Suppress("UNCHECKED_CAST")
constructor(properties: List<LottieDynamicProperty<*>>) : this(
Expand All @@ -104,6 +106,7 @@ class LottieDynamicProperties internal constructor(
properties.filter { it.property is ScaleXY } as List<LottieDynamicProperty<ScaleXY>>,
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>>,
)

internal fun addTo(drawable: LottieDrawable) {
Expand All @@ -125,6 +128,9 @@ class LottieDynamicProperties internal constructor(
intArrayProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
typefaceProperties.forEach { p ->
drawable.addValueCallback(p.keyPath, p.property, p.callback.toValueCallback())
}
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.airbnb.lottie.compose

import android.content.Context
import android.graphics.BitmapFactory
import android.graphics.Typeface
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
Expand All @@ -13,6 +14,7 @@ import com.airbnb.lottie.LottieComposition
import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieImageAsset
import com.airbnb.lottie.LottieTask
import com.airbnb.lottie.model.Font
import com.airbnb.lottie.utils.Logger
import com.airbnb.lottie.utils.Utils
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -45,6 +47,18 @@ import kotlin.coroutines.resumeWithException
* @param imageAssetsFolder A subfolder in `src/main/assets` that contains the exported images
* that this composition uses. DO NOT rename any images from your design tool. The
* filenames must match the values that are in your json file.
* @param fontAssetsFolder The default folder Lottie will look in to find font files. Fonts will be matched
* based on the family name specified in the Lottie json file.
* Defaults to "fonts/" so if "Helvetica" was in the Json file, Lottie will auto-match
* fonts located at "src/main/assets/fonts/Helvetica.ttf". Missing fonts will be skipped
* 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 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 @@ -55,6 +69,9 @@ import kotlin.coroutines.resumeWithException
fun rememberLottieComposition(
spec: LottieCompositionSpec,
imageAssetsFolder: String? = null,
fontAssetsFolder: String = "fonts/",
fontFileExtension: String = ".ttf",
fontRemapping: Map<String, String> = emptyMap(),
cacheComposition: Boolean = true,
onRetry: suspend (failCount: Int, previousException: Throwable) -> Boolean = { _, _ -> false },
): LottieCompositionResult {
Expand All @@ -69,6 +86,9 @@ fun rememberLottieComposition(
context,
spec,
imageAssetsFolder.ensureTrailingSlash(),
fontAssetsFolder.ensureTrailingSlash(),
fontFileExtension.ensureLeadingPeriod(),
fontRemapping,
cacheComposition,
)
result.complete(composition)
Expand All @@ -88,6 +108,9 @@ private suspend fun lottieComposition(
context: Context,
spec: LottieCompositionSpec,
imageAssetsFolder: String?,
fontAssetsFolder: String?,
fontFileExtension: String,
fontRemapping: Map<String, String>,
cacheComposition: Boolean,
): LottieComposition {
val task = when (spec) {
Expand All @@ -111,7 +134,10 @@ private suspend fun lottieComposition(
FileInputStream(spec.fileName)
}
when {
spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(ZipInputStream(fis), spec.fileName.takeIf { cacheComposition })
spec.fileName.endsWith("zip") -> LottieCompositionFactory.fromZipStream(
ZipInputStream(fis),
spec.fileName.takeIf { cacheComposition },
)
else -> LottieCompositionFactory.fromJsonInputStream(fis, spec.fileName.takeIf { cacheComposition })
}
}
Expand All @@ -129,6 +155,7 @@ private suspend fun lottieComposition(

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

Expand Down Expand Up @@ -198,8 +225,63 @@ private fun maybeDecodeBase64Image(asset: LottieImageAsset) {
}
}

private suspend fun loadFontsFromAssets(
context: Context,
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])
}
}
}

private fun maybeLoadTypefaceFromAssets(
context: Context,
font: Font,
fontAssetsFolder: String?,
fontFileExtension: String,
remappedFontPath: String?,
) {
val path = remappedFontPath ?: "$fontAssetsFolder${font.family}${fontFileExtension}"
val typefaceWithDefaultStyle = try {
Typeface.createFromAsset(context.assets, path)
} catch (e: Exception) {
Logger.error("Failed to find typeface in assets with path $path.", e)
return
}
try {
val typefaceWithStyle = typefaceForStyle(typefaceWithDefaultStyle, font.style)
font.typeface = typefaceWithStyle
} catch (e: Exception) {
Logger.error("Failed to create ${font.family} typeface with style=${font.style}!", e)
}
}

private fun typefaceForStyle(typeface: Typeface, style: String): Typeface? {
val containsItalic = style.contains("Italic")
val containsBold = style.contains("Bold")
val styleInt = when {
containsItalic && containsBold -> Typeface.BOLD_ITALIC
containsItalic -> Typeface.ITALIC
containsBold -> Typeface.BOLD
else -> Typeface.NORMAL
}
return if (typeface.style == styleInt) typeface else Typeface.create(typeface, styleInt)
}

private fun String?.ensureTrailingSlash(): String? = when {
this == null -> null
isNullOrBlank() -> null
endsWith('/') -> this
else -> "$this/"
}

private fun String.ensureLeadingPeriod(): String = when {
isBlank() -> this
startsWith(".") -> this
else -> ".$this"
}
3 changes: 3 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/LottieProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.graphics.ColorFilter;
import android.graphics.PointF;
import android.graphics.Typeface;

import com.airbnb.lottie.value.LottieValueCallback;
import com.airbnb.lottie.value.ScaleXY;
Expand Down Expand Up @@ -164,4 +165,6 @@ public interface LottieProperty {
ColorFilter COLOR_FILTER = new ColorFilter();

Integer[] GRADIENT_COLOR = new Integer[0];

Typeface TYPEFACE = Typeface.DEFAULT;
}
15 changes: 15 additions & 0 deletions lottie/src/main/java/com/airbnb/lottie/model/Font.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import static androidx.annotation.RestrictTo.Scope.LIBRARY;

import android.graphics.Typeface;

import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

@RestrictTo(LIBRARY)
Expand All @@ -12,6 +15,9 @@ public class Font {
private final String style;
private final float ascent;

@Nullable
private Typeface typeface;

public Font(String family, String name, String style, float ascent) {
this.family = family;
this.name = name;
Expand All @@ -34,4 +40,13 @@ public String getStyle() {
@SuppressWarnings("unused") float getAscent() {
return ascent;
}

@Nullable
public Typeface getTypeface() {
return typeface;
}

public void setTypeface(@Nullable Typeface typeface) {
this.typeface = typeface;
}
}
34 changes: 31 additions & 3 deletions lottie/src/main/java/com/airbnb/lottie/model/layer/TextLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class TextLayer extends BaseLayer {
setStyle(Style.STROKE);
}};
private final Map<FontCharacter, List<ContentGroup>> contentsForCharacter = new HashMap<>();
private final LongSparseArray<String> codePointCache = new LongSparseArray<String>();
private final LongSparseArray<String> codePointCache = new LongSparseArray<>();
private final TextKeyframeAnimation textAnimation;
private final LottieDrawable lottieDrawable;
private final LottieComposition composition;
Expand All @@ -71,6 +71,8 @@ public class TextLayer extends BaseLayer {
private BaseKeyframeAnimation<Float, Float> textSizeAnimation;
@Nullable
private BaseKeyframeAnimation<Float, Float> textSizeCallbackAnimation;
@Nullable
private BaseKeyframeAnimation<Typeface, Typeface> typefaceCallbackAnimation;

TextLayer(LottieDrawable lottieDrawable, Layer layerModel) {
super(lottieDrawable, layerModel);
Expand Down Expand Up @@ -236,8 +238,7 @@ private void drawGlyphTextLine(String text, DocumentData documentData, Matrix pa

private void drawTextWithFont(
DocumentData documentData, Font font, Matrix parentMatrix, Canvas canvas) {
float parentScale = Utils.getScale(parentMatrix);
Typeface typeface = lottieDrawable.getTypeface(font.getFamily(), font.getStyle());
Typeface typeface = getTypeface(font);
if (typeface == null) {
return;
}
Expand Down Expand Up @@ -298,6 +299,21 @@ private void drawTextWithFont(
}
}

@Nullable
private Typeface getTypeface(Font font) {
if (typefaceCallbackAnimation != null) {
Typeface callbackTypeface = typefaceCallbackAnimation.getValue();
if (callbackTypeface != null) {
return callbackTypeface;
}
}
Typeface drawableTypeface = lottieDrawable.getTypeface(font.getFamily(), font.getStyle());
if (drawableTypeface != null) {
return drawableTypeface;
}
return font.getTypeface();
}

private List<String> getTextLines(String text) {
// Split full text by carriage return character
String formattedText = text.replaceAll("\r\n", "\r")
Expand Down Expand Up @@ -517,6 +533,18 @@ public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> ca
textSizeCallbackAnimation.addUpdateListener(this);
addAnimation(textSizeCallbackAnimation);
}
} else if (property == LottieProperty.TYPEFACE) {
if (typefaceCallbackAnimation != null) {
removeAnimation(typefaceCallbackAnimation);
}

if (callback == null) {
typefaceCallbackAnimation = null;
} else {
typefaceCallbackAnimation = new ValueCallbackKeyframeAnimation<>((LottieValueCallback<Typeface>) callback);
typefaceCallbackAnimation.addUpdateListener(this);
addAnimation(typefaceCallbackAnimation);
}
}
}
}
Binary file not shown.
Binary file added sample-compose/src/main/assets/fonts/Roboto.ttf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.airbnb.lottie.sample.compose.examples.DynamicPropertiesExamplesPage
import com.airbnb.lottie.sample.compose.examples.ExamplesPage
import com.airbnb.lottie.sample.compose.examples.ImagesExamplesPage
import com.airbnb.lottie.sample.compose.examples.NetworkExamplesPage
import com.airbnb.lottie.sample.compose.examples.TextExamplesPage
import com.airbnb.lottie.sample.compose.examples.TransitionsExamplesPage
import com.airbnb.lottie.sample.compose.examples.ViewPagerExamplePage
import com.airbnb.lottie.sample.compose.lottiefiles.LottieFilesPage
Expand Down Expand Up @@ -100,6 +101,7 @@ class ComposeActivity : AppCompatActivity() {
composable(Route.NetworkExamples.route) { NetworkExamplesPage() }
composable(Route.DynamicPropertiesExamples.route) { DynamicPropertiesExamplesPage() }
composable(Route.ImagesExamples.route) { ImagesExamplesPage() }
composable(Route.TextExamples.route) { TextExamplesPage() }
composable(
Route.Player.fullRoute,
arguments = Route.Player.args
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ sealed class Route(val route: String, val args: List<NamedNavArgument> = emptyLi

object ImagesExamples : Route("image examples")

object TextExamples : Route("text examples")

object DynamicPropertiesExamples : Route("dynamic properties examples")

object Player : Route(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,11 @@ fun ExamplesPage(navController: NavController) {
modifier = Modifier
.clickable { navController.navigate(Route.ImagesExamples) }
)
ListItem(
text = { Text("Text") },
secondaryText = { Text("Using animations with text") },
modifier = Modifier
.clickable { navController.navigate(Route.TextExamples) }
)
}
}
Loading

0 comments on commit f6a7569

Please sign in to comment.