superus8r
superus8r

Reputation: 941

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

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

Upvotes: 23

Views: 20654

Answers (5)

Phil Dukhov
Phil Dukhov

Reputation: 88162

stringResource under the hood uses resources.getString, which discards any styled information.

Since 1.7.0 there's a new method to parse html to AnnotatedString, AnnotatedString.fromHtml, here's how you can use it to read styled resource string:

@Composable
@ReadOnlyComposable
fun annotatedStringResource(
    @StringRes id: Int,
): AnnotatedString {
    val text = LocalContext.current.resources.getText(id)
    val html = if (text is Spanned) {
        text.toHtml(TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
    } else {
        text.toString()
    }
    return AnnotatedString.fromHtml(html)
}

@Composable
@ReadOnlyComposable
fun annotatedStringResource(
    @StringRes id: Int,
    vararg formatArgs: Any,
): AnnotatedString {
    val text = LocalContext.current.resources.getText(id)
    val html = if (text is Spanned) {
        text.toHtml(TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
    } else {
        text.toString()
    }
    val encodedArgs = formatArgs.map { if (it is String) it.htmlEncode() else it }.toTypedArray()
    return AnnotatedString.fromHtml(html.format(*encodedArgs))
}

Note that it also has linkStyles and linkInteractionListener parameters that you may want to use to customize the result


Alternatively, you can use TextView that will handle it for you

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

Upvotes: 15

pls_hire_me
pls_hire_me

Reputation: 21

As others have said, stringResource(id = R.string.styled_text) will remove HTML tags. To prevent this, you can escape the angle bracket symbols using &lt; for the "less than" sign (<) and &gt; for the "greater than" sign (>). Your string resource would then be:

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

Upvotes: 0

Thiengo
Thiengo

Reputation: 304

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

Upvotes: 4

rewgoes
rewgoes

Reputation: 754

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

Upvotes: 16

Iv&#225;n Dar&#237;o
Iv&#225;n Dar&#237;o

Reputation: 11

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

Upvotes: 0

Related Questions