Jetpack Compose Theming: Typography — Part II
The directions and font features of text
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 byContent
. If text letters cannot be detected, the text will useLtr
as direction as a fallback.ContentOrRtl
: Text direction will be determined byContent
. If text letters cannot be detected, the text will useRtl
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)
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)
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)
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)
)
}
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 LocaleList
instead 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
The direction can be determined by content, Ltr
in both cases.
Text2: Arabic letters
The direction can be determined by content, Rtl
in both cases.
Text3: 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
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
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.
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.
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.