Variable Fonts are ready to use from Compose UI 1.3.0, for Android O and above.

A variable font is a font that can be customized in multiple ways (or axis or parameters) creating dramatically different styles, while living in a single font file. Those axes can be well known and defined in the standard (e.g.: weight, width, slant, italic) or added by the font itself. Learn more about variable fonts in Google Fonts Knowledge.

Text using the same variable font customized with different axes values.

By using variable fonts rather than regular font files you only have one font file to manage instead of multiple, containing all the possible font variations.
It allows designers to truly express the brand and developers to be as creative as the font allows, by accessing the whole range of styles with real flexibility to fine-tune it.

In your real app you will have one variable font and customize it to have 3–5 variations, as required by your designs, in a very simple way, always using the same font file.

In this blog we’ll go one step further. With variable fonts, we’ll be able to quickly build an app that dynamically changes the properties of the font and display it, like the following:

This app shows a text using a variable font, and multiple sliders to modify the font axes.

For this sample, we’ll be using a variable font called Roboto Flex designed by Font Bureau via Google Fonts.

But there’s many more variable fonts available from Google Fonts. You can find the whole catalog here together with a table of the supported axes per font.

Implementation in Compose

Setup

Firstly, we get the font. At the time of writing, variable fonts are not supported via downloadable fonts (it is an open feature request). So we need to download the font and bundle it in the APK. Make sure that the .ttf file you add is the variable fonts version of the font.

For the Roboto Flex font, the file will be named something like “RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf“ which we rename to “robotoflex_variable.ttf” to add it to Android.

Next up, we add the correct dependencies. Compose UI should be on 1.3.0 or above:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

// project build.gradle
buildscript {
   ext {
       compose_ui_version = '1.3.0-rc01'
   }
}

// module build.gradle
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-text-google-fonts:$compose_ui_version"

Now, onto the fun part 🎉.

If we check the variable fonts catalog again we’ll see that Roboto Flex can be customized by the following axes:

Axes for Roboto Flex shown in the Google Fonts website

This information is very important as it contains the different axes names (like XTRA, YTDE etc), min, max and default values. Check the Google Font glossary to know how each axis is able to configure the font.

We’ll select and implement only a subset of 8 Roboto Flex axes in our demo app, while the rest is equivalent.

We start by creating a Composable screen. We add a label and selector (or Slider) for each axis we want to showcase, and define the state for all these selectors by using <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState" target="_blank" rel="noopener ugc nofollow">MutableState<T></a>.

The <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#Slider(kotlin.Float,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.ranges.ClosedFloatingPointRange,kotlin.Int,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.SliderColors)" target="_blank" rel="noopener ugc nofollow">Slider</a> composable takes a valueRange parameter, that can be easily customized with the font information (min/max values) from the table above. This part of the app will look something like this:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

@Composable
fun MainScreen() {

   var weight by remember { mutableStateOf(400) }
   var slant by remember { mutableStateOf(0f) }
   var width by remember { mutableStateOf(100f) }
   var ascenderHeight by remember { mutableStateOf(750f) }
   var descenderDepth by remember { mutableStateOf(-203f) }
   var figureHeight by remember { mutableStateOf(738f) }
   var lowercaseHeight by remember { mutableStateOf(514f) }
   var uppercaseHeight by remember { mutableStateOf(712f) }

   val message = message

   Column(Modifier.padding(16.dp)) {
       Text("Weight")
       Slider(value = weight.toFloat(),
           onValueChange = { weight = it.toInt() },
           valueRange = 1f..1000f
       )

       Text("Slant")
       Slider(
           value = slant, onValueChange = { slant = it }, valueRange = -15f..0f
       )

       Text("Width")
       Slider(
           value = width, onValueChange = { width = it }, valueRange = 25f..151f
       )

       Text("Ascender Height")
       ...

   }

}

Up until this point, we have all our selectors configured like this:

Sliders configured for 8 axes for Roboto Flex

Predefined axis

Up next, we need to use the selector’s values and configure our font. For this we’ll use the new (experimental<a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/font/FontVariation" target="_blank" rel="noopener ugc nofollow">FontVariation</a> API.

For the predefined axis, <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/font/FontVariation.Settings" target="_blank" rel="noopener ugc nofollow">FontVariation</a> provides predefined methods. For example, we have <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/font/FontVariation#weight(kotlin.Int)" target="_blank" rel="noopener ugc nofollow">weight</a> and <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/font/FontVariation#slant(kotlin.Float)" target="_blank" rel="noopener ugc nofollow">slant</a> which can be set as follows:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontVariation

@OptIn(ExperimentalTextApi::class)
@Composable
fun MainScreen() {

   // selectors
   // ...

   Font(
       R.font.recursive,
       variationSettings = FontVariation.Settings(
           FontVariation.weight(weight),
           FontVariation.slant(slant),
           // ...
       )
   )

}

Where the values passed for weight and slant were previously defined as <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/runtime/State" target="_blank" rel="noopener ugc nofollow">State</a> coming from the <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#Slider(kotlin.Float,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.ranges.ClosedFloatingPointRange,kotlin.Int,kotlin.Function0,androidx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.material.SliderColors)" target="_blank" rel="noopener ugc nofollow">Slider</a>.

Custom axes

For the custom axes, you can use <a class="au le" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/font/FontVariation.Settings" target="_blank" rel="noopener ugc nofollow">FontVariation.Settings</a> method that receives the setting name and value as a parameter. The settings’ names and other relevant information were given by the Google variable fonts catalog we queried earlier. We can configure them like this:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

@OptIn(ExperimentalTextApi::class)
@Composable
fun MainScreen() {

   // selectors
   // ...

   val fontFamily = FontFamily(
       Font(
           R.font.recursive,
           variationSettings = FontVariation.Settings(
               FontVariation.weight(weight),
               FontVariation.slant(slant),
               FontVariation.width(width),
               FontVariation.Setting("YTAS", ascenderHeight),
               FontVariation.Setting("XTRA", counterWidth),
               FontVariation.Setting("YTDE", descenderDepth),
               FontVariation.Setting("YTFI", figureHeight),
               FontVariation.Setting("GRAD", grade),
               FontVariation.Setting("YTLC", lowercaseHeight),
               FontVariation.Setting("YOPQ", thinStroke),
               FontVariation.Setting("YTUC", uppercaseHeight)
           )
       )
   )

   Text(
       text = message,
       fontFamily = fontFamily
   )

}

All together now

Finally, we add the new Font to the font family. We create a Text composable, and set the font family. Our main screen will look like this:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

@OptIn(ExperimentalTextApi::class)
@Composable
fun MainScreen() {

   // selectors
   // ...

   val fontFamily = FontFamily(
       Font(
           R.font.recursive,
           variationSettings = FontVariation.Settings(
               FontVariation.weight(weight),
               FontVariation.slant(slant),
               FontVariation.width(width),
               FontVariation.Setting("YTAS", ascenderHeight),
               FontVariation.Setting("XTRA", counterWidth),
               FontVariation.Setting("YTDE", descenderDepth),
               FontVariation.Setting("YTFI", figureHeight),
               FontVariation.Setting("GRAD", grade),
               FontVariation.Setting("YTLC", lowercaseHeight),
               FontVariation.Setting("YOPQ", thinStroke),
               FontVariation.Setting("YTUC", uppercaseHeight)
           )
       )
   )

   Text(
       text = message,
       fontFamily = fontFamily
   )

}

In a real app, you’ll probably define the font family in your <em class="jj">Type.kt</em> file with static values as opposed to snapshot state variables.

And that’s it! We have our sample app finished, showing how the Roboto Flex variable font is stylized by all its axes.

App completed with sliders configuring 8 axes for Roboto Flex, and text dynamically reacting to the font changes.

Best practices

There are two improvements we can do to our code.

For custom axis, we can wrap each in a validation function like the following:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

Font(
   R.font.robotoflex_variable,
   variationSettings = FontVariation.Settings(
       …
       FontVariation.ascenderHeight(ascenderHeight),
       …
   )
)

// Type.kt
@OptIn(ExperimentalTextApi::class)
fun FontVariation.ascenderHeight(ascenderHeight: Float): FontVariation.Setting {
   require(ascenderHeight in 649f..854f) { "'Ascender Height' must be in 649f..854f" }
   return FontVariation.Setting("YTAS", ascenderHeight)
}

This refactor has a few benefits. Most notably, it validates and prevents misconfiguration of the font by other developers. It also results in a cleaner, more idiomatic syntax altogether.

And secondly, variable fonts are not supported before Android O, therefore this code will crash your app if run in older versions with a runtime IllegalStateException. To prevent this, we can add a guardrail with a fallback font like this:

/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */

val fontFamily = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
   FontFamily(...)
} else {
   FontFamily(
            Font(R.font.robotoflex_static_regular)
        )
}

Recap

Hope you learned something new, and can incorporate the power of variable fonts to your app.

If you find any bugs or have a feature request, please let us know by filing an issue on our tracker.

Finally, drop all your questions here or on Twitter @astamatok or mastodon and share what you’re building using variable fonts!

Happy Composing! 👋

This post was written with the collaboration of Sean McQuillan on the Jetpack Compose Text team. Thanks to Rebecca Franks and Florina Muntenescu on the DevRel team for their reviews.

By Alejandra Stamato
Source Android

Previous The New Apple Pacific Centre Opens In Vancouver
Next Faster Jetpack Compose View Interop With App Startup And Baseline Profile