Let’s have some fun with Fonts: part 2
This article is the second part of the series that will smoothly introduce the topic of fonts to you.
Part 1 can be found here
In the first part, we saw how we could add fonts to the project the hard way. However, fonts these days are more capable than rendering single glyphs.
Roboto Slab
We can find many fonts on Google Fonts that support what is called Variable Fonts. The most basic possibility gives it to change font weight(wght), slant(slnt) or make it italic(ital). The most simple example might be Roboto Slab, which can only dynamically change its weight.
val weights = listOf(
FontWeight.Thin,
FontWeight.Light,
FontWeight.Normal,
FontWeight.Medium,
FontWeight.Bold,
FontWeight.Black,
)
@Composable
fun VariableSlabQuickNEasy() = Column {
val slabFontFamily = FontFamily(Font(R.font.roboto_slab))
weights.forEach { weight ->
Text(
text = demoText,
fontFamily = slabFontFamily,
fontWeight = weight,
)
Once again, we see the same problem that was described in the previous post. Framework tried to adjust the weight to the given font instead of taking advantage of our variable font. So, let’s fix it.
private val slabFontFamily = weights.map {
Font(
R.font.roboto_slab,
weight = it,
variationSettings = FontVariation.Settings(FontVariation.weight(it.weight))
)
}.let { FontFamily(it) }
@Composable
fun VariableSlab() = Column {
weights.forEach { weight ->
Text(
text = demoText,
fontFamily = slabFontFamily,
fontWeight = weight,
)
}
}
Much better. Here we are assigning to each weight proper font variation using FontVariation.Settings.
Flexing ?
As you might expect, FontVariation has much more to offer, let’s look into Roboto Flex. Here we can alter over 10 axes. With that, designers can create their own look and feel using the same font.
@Composable
fun VariableFlex() = Column {
Text(
text = "Original",
fontFamily = FontFamily(
Font(
R.font.roboto_flex,
)
),
)
Text(
text = "Width weight",
fontFamily = FontFamily(
Font(
R.font.roboto_flex,
variationSettings = FontVariation.Settings(
FontVariation.weight(580),
FontVariation.width(50f),
)
)
),
)
Text(
text = "Width weight + grade",
fontFamily = FontFamily(
Font(
R.font.roboto_flex,
variationSettings = FontVariation.Settings(
FontVariation.weight(400),
FontVariation.width(50f),
FontVariation.grade(78),
)
)
),
)
Text(
text = "Very funky font",
fontFamily = FontFamily(
Font(
R.font.roboto_flex,
variationSettings = FontVariation.Settings(
FontVariation.weight(400),
FontVariation.width(50f),
FontVariation.grade(78),
FontVariation.Setting("YOPQ", 106f),
FontVariation.Setting("YTLC", 451f),
FontVariation.Setting("YTDE", -305f),
)
)
),
)
}
The first few examples are quite simple, but last, we used a bit more unusual variation. We change axes like:
- Descender Depth (YTDE) – A parametric axis for varying the depth of lowercase descenders.
- Lowercase Height (YTLC) – A parametric axis for varying the height of the lowercase.
- Thin Stroke (YOPQ) – A parametric axis for varying thin stroke weights, such as bars and hairlines.
You can learn more about the widely used params here: https://fonts.google.com/knowledge/glossary and here: https://fonts.google.com/knowledge/using_type/implementing_open_type_features_on_the_web#common-open-type-feature-reference. Moreover, the font can define its own unique axes take a look for example into: https://github.com/googlefonts/decovar#axes. Countless possibilities!
There is More™
Previously we explored general typeface changes, but we can also change how individual glyphs looks in narrow context. Let’s see an example.
@Composable
fun FontFeature() = Column {
val text = buildAnnotatedString {
append("0")
withStyle(SpanStyle(fontFeatureSettings = "zero")) {
append("0n")
}
append("subs & dnom")
withStyle(SpanStyle(fontFeatureSettings = "subs")) {
append("0")
}
withStyle(SpanStyle(fontFeatureSettings = "dnom")) {
append("0n")
}
append("1234567890n")
withStyle(SpanStyle(fontFeatureSettings = "ss01")) {
append("1234567890n")
}
withStyle(SpanStyle(fontFeatureSettings = "frac,dlig")) {
append("3/4 !?")
}
}
Text(
text = text,
fontFamily = FontFamily(Font(R.font.inter)),
)
Text(
text = text,
fontFamily = FontFamily(
Font(
R.font.inter,
variationSettings = FontVariation.Settings(
FontVariation.weight(800),
FontVariation.slant(-10f),
)
)
),
)
}
@Composable
fun Swash() = Column {
Text(
text = "OpenType Variable Fonts",
fontFamily = FontFamily(Font(R.font.sansita_swashed)),
)
val annotatedString = buildAnnotatedString {
withStyle(style = SpanStyle(fontFeatureSettings = "swsh,salt")) {
append("OpenType Variable Fonts")
}
}
Text(
text = annotatedString,
fontFamily = FontFamily(Font(R.font.sansita_swashed)),
)
}
Here we are using `fontFeatureSettings` to enable font features. In this example, we use a font called Inter, which features are nicely listed here https://rsms.me/inter/#features. The last example shows how we can join features.
Limitation
Unfortunately, it works only on Android Oreo (API 26) and above. On lower versions OS won’t be able to load font and will crash with:
android.content.res.Resources$NotFoundException: Font resource ID #0x7f060010 could not be retrieved.
at androidx.core.content.res.ResourcesCompat.loadFont(ResourcesCompat.java:574)
at androidx.core.content.res.ResourcesCompat.getFont(ResourcesCompat.java:411)
at androidx.compose.ui.text.font.AndroidFontLoader_androidKt.load(AndroidFontLoader.android.kt:75)
Additional information:
- Nice source of general information: https://fonts.google.com/knowledge
- Additional base of variable fonts: https://v-fonts.com/
- Great playground: https://www.axis-praxis.org/
- Immortal presentation by @chiuki https://chiuki.github.io/advanced-android-textview/