Jetpack Compose Text
Composable features a letterSpacing
parameter, which according to the documentation is described as:
the amount of space to add between each letter
However, in English scripts like many Latin/Roman scripts letters are not joined together and are separate.
@Preview(showBackground = true)
@Composable
fun SomeText() {
Text(
text = "Hello there!",
letterSpacing = 5.sp
)
}
In Persian/Arabic scripts sometimes letters are joined together and letters take different shapes according to the place they are in the word, so using the letterSpacing
parameter should do the same however somehow Jetpack Compose cannot make letters far from each other when letterSpacing
is increased instead it increases the distance between the words which is an unwanted behavior and even a bug in my opinion.
In this example, I even choose words that mostly have separated letters but still, Jetpack Compose cannot detect them as letters.
This is a three-word sentence meaning, Greetings from Iran.
@Preview(showBackground = true)
@Composable
fun SomeText() {
Text(
text = "درود از ایران",
letterSpacing = 5.sp
)
}
The same text with more letter spacing, however, it's converted to word spacing.
@Preview(showBackground = true)
@Composable
fun SomeText() {
Text(
text = "درود از ایران",
letterSpacing = 50.sp
)
}
You may ask what's the correct/desired behavior. I can show you it using Photoshop.
Zero letter spacing in Photoshop
200-letter spacing in Photoshop
So the question is how to overcome this problem and add true letter spacing for Persian/Arabic texts.
I tried reporting this as a bug to Google's issue tracker but it was ignored and responded to me that it's intended behavior by referencing W3 standards and they don't fix it since this behavior is the same for Text views, however, in the same link to the W3 standards there are two appropriate examples required by them but Android's team choose the simpler example to implement.
I understand this is an edge case and even W3 standards indicated that but it can be implemented. So I implemented it by adding U+0640 ـ ARABIC TATWEEL
between certain characters by demand of the letterSpacing parameter, the result and code are not perfect but it works.
@Composable
fun LetterSpacedPersianText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = 14.sp,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) {
val isPersian = remember { Regex("[ء-ی]") }
val keshides = letterSpacing.value.toInt()
val keshidesText = buildString { repeat(keshides) { append('ـ') } }
val spacesText = buildAnnotatedString {
repeat(keshides) {
withStyle(style = SpanStyle(fontSize = fontSize.div(50 * letterSpacing.value))) {
append(' ')
}
}
}
if (isPersian.containsMatchIn(text)) {
if (text.length != 1) {
val totalCursive = "ئبپتثجچحخسشصضطظعغفقکگلمنهی"
val finalCursive = "أؤدذرزژو"
val newText = buildAnnotatedString {
var i = 0
while (i < text.length) {
val current = text[i]
append(current)
val next = text.getOrNull(i + 1)
if (next != null) {
if (totalCursive.contains(current) && totalCursive.contains(next) ||
totalCursive.contains(current) && finalCursive.contains(next)
) {
append(keshidesText)
}
if (current !in totalCursive && current !in finalCursive && next !in totalCursive && next !in finalCursive) {
append(spacesText)
}
}
i++
}
}
Text(
text = newText,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
} else {
Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}
} else {
Text(
text = text,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
onTextLayout = onTextLayout,
style = style
)
}
}
Example:
@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "درود از ایران",
letterSpacing = 10.sp
)
}
@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "درود از ایران",
letterSpacing = 1.sp
)
}
@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "بسیار عالی",
letterSpacing = 1.sp
)
}
@Preview(showBackground = true)
@Composable
fun SomeText() {
LetterSpacedPersianText(
text = "بسیار عالی",
letterSpacing = 10.sp
)
}