Android Jetpack Compose: How to show styled Text from string resources
Asked Answered
N

4

19

I have a string in my strings.xml which is localized for different languages. The strings are styled with Html tags for each localization.

Using Android TextView, I was able to show the styled text just fine by reading the string resources.

Considering that Jetpack Compose currently (1.0.0-rc02) does not support Html tags, I tried using TextView inside an AndroidView composable following Official Docs: https://developer.android.com/jetpack/compose/interop/interop-apis#views-in-compose

Example of what I tried:

@Composable
fun StyledText(text: String, modifier: Modifier = Modifier) {
    AndroidView(
            modifier = modifier,
            factory = { context -> TextView(context) },
            update = {
                it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
            }
    )
}

The text in strings.xml file:

<string name="styled_text">Sample text with <b>bold styling</b> to test</string>

However, using stringResource(id = R.string.styled_text) provides the text without the Html tags.

Is there a way to show text from string resources with Html styles in Jetpack Compose?


The following two questions are similar, but they do not read the string from resources:

Jetpack compose display html in text

Android Compose: How to use HTML tags in a Text view

Newscast answered 27/7, 2021 at 17:18 Comment(0)
K
13

stringResource under the hood uses resources.getString, which discards any styled information. You need to create something like textResource to get the raw value:

@Composable
@ReadOnlyComposable
fun textResource(@StringRes id: Int): CharSequence =
    LocalContext.current.resources.getText(id)

And use it like this:

StyledText(textResource(id = R.string.foo))

@Composable
fun StyledText(text: CharSequence, modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context -> TextView(context) },
        update = {
            it.text = text
        }
    )
}
Kithara answered 27/7, 2021 at 18:7 Comment(5)
Yes, it works for old android TextView just fine! stringResource seems to skip the Html tags completely.Newscast
thank you so much, it worked! Wrapping up what I understood from your answer: using resources.getText() returns a char sequence if the resource is styled. Also, we still have to use the View-based TextView. Well done! For everyone else, more info about resources.getText() in official docs: developer.android.com/reference/android/content/res/…Newscast
Just be aware that this does not use the theme's typography.Pushed
For anyone needing the same for array string resources, use: @Composable @ReadOnlyComposable fun textArrayResource(@ArrayRes id: Int): Array<CharSequence> { val resources = LocalContext.current.resources return resources.getTextArray(id) }Mauritius
nice solution. I extended textResource by .replace("\n +".toRegex(),"\n").trim()Terra
P
16

There is ongoing discussion to implement on Jetpack Compose UI: https://issuetracker.google.com/issues/139320238

After some research, I came me up with the following solution that I also posted in the same discussion:

@Composable
@ReadOnlyComposable
private fun resources(): Resources {
    LocalConfiguration.current
    return LocalContext.current.resources
}

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)
}

@Composable
fun annotatedStringResource(@StringRes id: Int, vararg formatArgs: Any): AnnotatedString {
    val resources = resources()
    val density = LocalDensity.current
    return remember(id, formatArgs) {
        val text = resources.getText(id, *formatArgs)
        spannableStringToAnnotatedString(text, density)
    }
}

@Composable
fun annotatedStringResource(@StringRes id: Int): AnnotatedString {
    val resources = resources()
    val density = LocalDensity.current
    return remember(id) {
        val text = resources.getText(id)
        spannableStringToAnnotatedString(text, density)
    }
}

private fun spannableStringToAnnotatedString(
    text: CharSequence,
    density: Density
): AnnotatedString {
    return if (text is Spanned) {
        with(density) {
            buildAnnotatedString {
                append((text.toString()))
                text.getSpans(0, text.length, Any::class.java).forEach {
                    val start = text.getSpanStart(it)
                    val end = text.getSpanEnd(it)
                    when (it) {
                        is StyleSpan -> when (it.style) {
                            Typeface.NORMAL -> addStyle(
                                SpanStyle(
                                    fontWeight = FontWeight.Normal,
                                    fontStyle = FontStyle.Normal
                                ),
                                start,
                                end
                            )
                            Typeface.BOLD -> addStyle(
                                SpanStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontStyle = FontStyle.Normal
                                ),
                                start,
                                end
                            )
                            Typeface.ITALIC -> addStyle(
                                SpanStyle(
                                    fontWeight = FontWeight.Normal,
                                    fontStyle = FontStyle.Italic
                                ),
                                start,
                                end
                            )
                            Typeface.BOLD_ITALIC -> addStyle(
                                SpanStyle(
                                    fontWeight = FontWeight.Bold,
                                    fontStyle = FontStyle.Italic
                                ),
                                start,
                                end
                            )
                        }
                        is TypefaceSpan -> addStyle(
                            SpanStyle(
                                fontFamily = when (it.family) {
                                    FontFamily.SansSerif.name -> FontFamily.SansSerif
                                    FontFamily.Serif.name -> FontFamily.Serif
                                    FontFamily.Monospace.name -> FontFamily.Monospace
                                    FontFamily.Cursive.name -> FontFamily.Cursive
                                    else -> FontFamily.Default
                                }
                            ),
                            start,
                            end
                        )
                        is BulletSpan -> {
                            Log.d("StringResources", "BulletSpan not supported yet")
                            addStyle(SpanStyle(), start, end)
                        }
                        is AbsoluteSizeSpan -> addStyle(
                            SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()),
                            start,
                            end
                        )
                        is RelativeSizeSpan -> addStyle(
                            SpanStyle(fontSize = it.sizeChange.em),
                            start,
                            end
                        )
                        is StrikethroughSpan -> addStyle(
                            SpanStyle(textDecoration = TextDecoration.LineThrough),
                            start,
                            end
                        )
                        is UnderlineSpan -> addStyle(
                            SpanStyle(textDecoration = TextDecoration.Underline),
                            start,
                            end
                        )
                        is SuperscriptSpan -> addStyle(
                            SpanStyle(baselineShift = BaselineShift.Superscript),
                            start,
                            end
                        )
                        is SubscriptSpan -> addStyle(
                            SpanStyle(baselineShift = BaselineShift.Subscript),
                            start,
                            end
                        )
                        is ForegroundColorSpan -> addStyle(
                            SpanStyle(color = Color(it.foregroundColor)),
                            start,
                            end
                        )
                        else -> addStyle(SpanStyle(), start, end)
                    }
                }
            }
        }
    } else {
        AnnotatedString(text.toString())
    }
}

Source: https://issuetracker.google.com/issues/139320238#comment11

With these helper methods, you can simply call:

Text(annotatedStringResource(R.string.your_string_resource))
Pushed answered 29/11, 2021 at 23:15 Comment(3)
So what you're saying is that I can use it in a Text Composable?Tuinenga
Yes, since Text supports AnnotatedString, you can call Text(annotatedStringResource(R.string.your_string_resource)), which was defined in the answer above.Pushed
@superus8r, could you please reconsider what the correct answer it? I don't think that using AndroidView is the best solution. The answer provided here uses only Jetpack Compose.Pushed
K
13

stringResource under the hood uses resources.getString, which discards any styled information. You need to create something like textResource to get the raw value:

@Composable
@ReadOnlyComposable
fun textResource(@StringRes id: Int): CharSequence =
    LocalContext.current.resources.getText(id)

And use it like this:

StyledText(textResource(id = R.string.foo))

@Composable
fun StyledText(text: CharSequence, modifier: Modifier = Modifier) {
    AndroidView(
        modifier = modifier,
        factory = { context -> TextView(context) },
        update = {
            it.text = text
        }
    )
}
Kithara answered 27/7, 2021 at 18:7 Comment(5)
Yes, it works for old android TextView just fine! stringResource seems to skip the Html tags completely.Newscast
thank you so much, it worked! Wrapping up what I understood from your answer: using resources.getText() returns a char sequence if the resource is styled. Also, we still have to use the View-based TextView. Well done! For everyone else, more info about resources.getText() in official docs: developer.android.com/reference/android/content/res/…Newscast
Just be aware that this does not use the theme's typography.Pushed
For anyone needing the same for array string resources, use: @Composable @ReadOnlyComposable fun textArrayResource(@ArrayRes id: Int): Array<CharSequence> { val resources = LocalContext.current.resources return resources.getTextArray(id) }Mauritius
nice solution. I extended textResource by .replace("\n +".toRegex(),"\n").trim()Terra
S
3

To keep using stringResources() you can do as down below.

1st - Use <![CDATA[ … ]]>

Embrace all your resource HTML strings between <![CDATA[ … ]]> then you are not going to lose the HTML definitions when calling stringResources():

<string name="styled_text"><![CDATA[Sample text with <b>bold styling</b> to test]]></string>

2nd - Create a "from Spanned to AnnotatedString processor"

First of all thanks to this awesome answer.

Then it is important to know that the text param (from Text() composable) does also accept an AnnotatedString object and not only a String.

Moving forward… we can create a "from Spanned to AnnotatedString processor" algorithm using the Jetpack buildAnnotatedString().

In this case I would pick the "create the algorithm as an extension private function inside of the client composable file" path:

private fun Spanned.toAnnotatedString(): AnnotatedString =
    buildAnnotatedString {
        val spanned = this@toAnnotatedString
        append(spanned.toString())

        getSpans(
            0,
            spanned.length,
            Any::class.java
        ).forEach { span ->
            val start = getSpanStart(span)
            val end = getSpanEnd(span)

            when (span) {
                is StyleSpan ->
                    when (span.style) {
                        Typeface.BOLD -> addStyle(
                            SpanStyle(fontWeight = FontWeight.Bold),
                            start,
                            end
                        )

                        Typeface.ITALIC -> addStyle(
                            SpanStyle(fontStyle = FontStyle.Italic),
                            start,
                            end
                        )

                        Typeface.BOLD_ITALIC -> addStyle(
                            SpanStyle(
                                fontWeight = FontWeight.Bold,
                                fontStyle = FontStyle.Italic
                            ),
                            start,
                            end
                        )
                    }

                is ForegroundColorSpan -> addStyle( // For <span style=color:blue> tag.
                    SpanStyle(color = Color.Blue),
                    start,
                    end
                )
            }
        }
    }

3rd - Call HtmlCompat.fromHtml()

As a last step before sending the AnnotatedString to the Text() composable we need to call the target String on HtmlCompat.fromHtml() method and also the new toAnnotatedString() extension function:

val textAsAnnotatedString = HtmlCompat.fromHtml(
    stringResource(id = R.string.styled_text),
    HtmlCompat.FROM_HTML_MODE_COMPACT
).toAnnotatedString()

4th - Show it on Text()

And then just show it on your target Text() composable:

Text(text = textAsAnnotatedString)

Note: you can add lot's of "style interpreter" inside of toAnnotatedString().

The down below print (everything inside of the red rectangle) is from a composable SnackBar on my Android project using the same above strategy.

enter image description here

Streusel answered 18/10, 2023 at 21:39 Comment(1)
Thank you for solution. @Streusel Do you know how can I provide support for <li>, <ol>, <ul> in this compose TextView?Impaction
J
-1

Currently, you can use AnnotatedString class in order to show styled texts within a Text component. I will show you an example:

Text(
    text = buildAnnotatedString {
        withStyle(style = SpanStyle(fontWeight = FontWeight.Thin)) {
            append(stringResource(id = R.string.thin_string))
        }
        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
            append(stringResource(id = R.string.bold_string))
        }
    },
)

Depending on the style you assign, you will get a text with the style of the red highlight

https://i.sstatic.net/XSMMB.png

For more info: https://developer.android.com/jetpack/compose/text?hl=nl#multiple-styles

Jolson answered 28/10, 2021 at 15:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.