Skip to content

Instantly share code, notes, and snippets.

@apkelly
Last active February 1, 2024 18:53
Show Gist options
  • Save apkelly/53dfd4debd3e2ad588c2c7ddeebf2756 to your computer and use it in GitHub Desktop.
Save apkelly/53dfd4debd3e2ad588c2c7ddeebf2756 to your computer and use it in GitHub Desktop.
Annotated String resources in Kotlin and for Jetpack Compose UI

The code below renders the following text in both Android Views and in Jetpack Compose UI.

Contact our team on 555 555 555 Opt 3 to activate.

@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
val resources = LocalContext.current.resources
return remember(id) {
val text = resources.getText(id, *formatArgs)
spannableStringToAnnotatedString(text)
}
}
@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
val resources = LocalContext.current.resources
return remember(id) {
val text = resources.getText(id)
spannableStringToAnnotatedString(text)
}
}
private fun spannableStringToAnnotatedString(text: CharSequence): AnnotatedString {
return if (text is Spanned) {
val spanStyles = mutableListOf<AnnotatedString.Range<SpanStyle>>()
spanStyles.addAll(text.getSpans(0, text.length, UnderlineSpan::class.java).map {
AnnotatedString.Range(
SpanStyle(textDecoration = TextDecoration.Underline),
text.getSpanStart(it),
text.getSpanEnd(it)
)
})
spanStyles.addAll(text.getSpans(0, text.length, StyleSpan::class.java).map {
AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
text.getSpanStart(it),
text.getSpanEnd(it)
)
})
AnnotatedString(text.toString(), spanStyles = spanStyles)
} else {
AnnotatedString(text.toString())
}
}
<string name="launch_awaiting_instructions">Contact <b>our</b> team on %1$s to activate.</string>
<string name="support_contact_phone_number"><b>555 555 555</b> Opt <b>3</b></string>
fun Spanned.toHtmlWithoutParagraphs(): String {
return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE)
.substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>")
}
fun Resources.getText(@StringRes id: Int, vararg args: Any): CharSequence {
val escapedArgs = args.map {
if (it is Spanned) it.toHtmlWithoutParagraphs() else it
}.toTypedArray()
val resource = SpannedString(getText(id))
val htmlResource = resource.toHtmlWithoutParagraphs()
val formattedHtml = String.format(htmlResource, *escapedArgs)
return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
@alekseichuk
Copy link

I think the @ReadOnlyComposable should eliminate need for remember.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment