Jetpack Compose Theming: Typography — Part II

The directions and font features of text

Gözde Kaval
8 min readJul 3, 2022
Photo by Alexander Andrews on Unsplash

In this article, I will be focusing on text directions, localication, and Font-related properties of Typography. If you want to see Part I, check here.

textDirection & localeList

Text direction indicates the direction of the text. It has 5 different values. LocaleList is also use to determine the direction.

  • Ltr: Text will be aligned from left to right.
  • Rtl: Text will be aligned from right to left.
  • Content: Text direction will be determined by the first strong directional character. For example, if the text consists of the Latin alphabet letters, the text will be aligned from left to right since Latin letters are used in LTR language. If the text is in an RTL language letters such as Arabic, the text will be aligned from right to left.
  • ContentOrLtr: Text direction will be determined by Content. If text letters cannot be detected, the text will use Ltr as direction as a fallback.
  • ContentOrRtl: Text direction will be determined by Content. If text letters cannot be detected, the text will use Rtl as direction as a fallback.

Let's check with an example. Here we will set 3 texts with all directions and observe the difference.

Text1: Latin letters (Strong characters)

Latin letters

As we see, Ltr and Rtl directions will always be fixed — no matter what the content is. The direction of the remaining lines will be determined by content, fallbacks will be not used.

Text2: Arabic letters (مرحبا بالعالم means “Hello World” according to Google Translate — Strong characters)

Arabic letters

As we see, Ltr and Rtl directions will always be fixed — no matter what the content is. The direction of the remaining lines will be determined by content, fallbacks will be not used.

Text3: Weak characters (Ref:https://en.wikipedia.org/wiki/Bidirectional_text)

Numbers. The content direction cannot be determined

As we see, Ltr and Rtl directions will always be fixed — no matter what the content is. The direction of ContentOrLtr and ContentOrRtl are determined by fallback, Ltr and Rtl respectively since it cannot be determined by content.

So, the question is, how the direction is determined for Content in this case, since there is no fallback option? The answer is LayoutDirection. As we know, all jetpack components are having default layout directions: either LayoutDirection.Ltr or LayoutDirection.Rtl. In this example, the default layout direction is Ltr, therefore the middle text direction is chosen as Ltr.

Let's choose the direction as LayoutDirection.Rtl and check the values:

CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl) {
Text(
text = "Text",
style = MaterialTheme.typography.body1.copy(
textDirection = textDirection
),
modifier = Modifier
.fillMaxWidth()
.weight(5f)
)
}
Neutral Text — LayoutDirection: Rtl

As we see, text direction is Rtl now for Content case since the default layout direction is Rtl.

To sum: Determined the direction via content if it's not fixed one (Ltr,Rtl). If the direction cannot be determined from content, use fallback if exist(ContentOrLtr, ContentOrRtl). If fallback does not exist, use LayoutDirection.(Content)

How the localeList is related to direction then? Let’s see the explanation of TextDirection.Content:

This value indicates that the text direction depends on the first strong directional character in the text according to the Unicode Bidirectional Algorithm. If no strong directional character is present, then androidx.compose.ui.unit.LayoutDirection is used to resolve the final TextDirection.
if used while creating a Paragraph object, androidx.compose.ui.text.intl.LocaleList will be used to resolve the direction as a fallback instead of androidx.compose.ui.unit.LayoutDirection.

So, if we use Paragraph object and choose direction as TextDirection.Content, the direction will use LocaleListinstead of LayoutDirection as fallback.

According to the official document, a paragraph and drawn on a Canvas.

Create a paragraph and add English Locale (LTR) to LocaleList.

val paragraph1 = Paragraph(
paragraphIntrinsics = ParagraphIntrinsics(
text = "TEXT",
style = MaterialTheme.typography.body1.copy(
localeList = LocaleList(
listOf(
Locale("en")
)
)
,
textDirection = TextDirection.Content
),
density = Density(LocalContext.current),
fontFamilyResolver = createFontFamilyResolver(LocalContext.current)
),
width = 1000f
)

And we will create a second paragraph with Arabic Locale(RTL).

val paragraph2 = Paragraph(
paragraphIntrinsics = ParagraphIntrinsics(
....
style = MaterialTheme.typography.body1.copy(
localeList = LocaleList(
listOf(
Locale("ar")
)
)
,
textDirection = TextDirection.Content
),
......
)

And last, we will draw both paragraphs on canvas.

Canvas(modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.border(width = 1.dp, color = Color.Red)) {
paragraph.paint(canvas = drawContext.canvas, color = Color.Blue)

}

We will use the same texts as before to compare. Here are the results:

Text1: Latin letters

Latin Letters

The direction can be determined by content, Ltr in both cases.

Text2: Arabic letters

Arabic Letters

The direction can be determined by content, Rtl in both cases.

Text3: Neutral letters

Neutral Letters

The direction cannot be determined by content, therefore localeList is used as the fallback option. In the first case, the locale is English so the text will be Ltr, in second case, the locale is Arabic, the text will be Rtl.

Now, we will continue with font-related properties.

fontSize

Size of the text. Must be implemented with sp extension as we used to do on xml.

  • First text: fontSize = 12.sp
  • Second text: fontSize = 30.sp
Different font sizes

fontWeight

Weight of the text. There are 9 predefined values (weight increase by 100). We can also create our own with a weight value between 1–1000.

fontWeight = FontWeight.Bold

fontWeight = FontWeight(weight = 1000)

fontStyle

Two types of font styles. Even if FontStyle has value, it’s not possible to customize it. Here is the TODO :)

// TODO(b/205312869) This constructor should not be public as it leads to FontStyle([cursor]) in AS

  • First text : fontStyle = FontStyle.Normal
  • Second text : fontStyle = FontStyle.Italic

fontFamily

Font family can be also selected on style. There is 5 default one:

fontFamily = FontFamily.Default
fontFamily = FontFamily.Monospace
Default Font Families

We can also add custom fonts. Google provides many custom fonts on this website. Let’s download one and add the ttf files under res>font folder.

Canterell font family

This font comes with different font weights and styles. We need to add our fonts accordingly. If the font name contains weight and style, we need to add them while creating the Font class. After every font is set, we create our FontFamily and give it to TextStyle.

val customFont = FontFamily(
Font(
R.font.cantarell_regular,
weight = FontWeight.Normal,
style = FontStyle.Normal
),
Font(
R.font.cantarell_bolditalic,
weight = FontWeight.Bold,
style = FontStyle.Italic
),
Font(
R.font.cantarell_italic,
weight = FontWeight.Normal,
style = FontStyle.Italic
),
Font(
R.font.cantarell_bold,
weight = FontWeight.Bold,
style = FontStyle.Normal
),
)

In this case, chosen font is R.font.cantarell_regular since fontWeight and fontStyle are using default. If we use different weights and styles, the chosen font will be different accordingly.

fontSynthesis

If a font doesn’t contain font style and font weight properties, the Android system adds fake values automatically. Even if the custom font doesn't have different ttf files for bold and italic types, we can still see texts bold or italic. It is the default behavior and its controlled by FontSynthesis. FontSynthesis allows Android system to use fake font weights and font styles. Default value is FontSynthesis.All , means fake values are allowed by default.

To understand different synthesis values, let's remove styles and weights from our custom font first and keep only the regular one.

val customFont = FontFamily(
Font(
R.font.cantarell_regular,
weight = FontWeight.Normal,
style = FontStyle.Normal
),
// Other fonts are removed
)

Let's see what will happen for bold and italic texts by default.

Even if our font doesn't contain any different ttf files for bold and italic, system uses fake ones.

If we enable it only for fontWeight, fontStyle will not have any effect on texts.

There are no italic texts since font synthesis is FontSynthesis.Weight

If we enable it only for fontStyle, fontWeight will not have any effect on texts.

There are no italic texts since font synthesis is FontSynthesis.Style

Let’s disable for all, then there will be no bold or italic texts.

fontFeatureSettings

There are some advanced CSS feature settings which we can use in style to show different cases. Settings can be found here.

Setting: <Text>

We go through material design typography and all properties of text style. There are some key points to consider:

  • Create style colors (textColor, background, shadow) by theme instead of hard-coded ones.
  • Use start and end alignments instead of left and right since some languages are right to left direction.
  • Don't prefer fixed text directions (Ltr, Rtl) if you support multiple languages.
  • Typography is very powerful :) Consider checking different font feature settings for different purposes.
  • Avoid very thin weight values since they are barely visible.
  • Very small font sizes are very hard to read, use at least 12.sp.
  • And of course, always read your text style from MaterialTheme.typography to avoid duplications and possible bugs.

--

--