Android Jetpack Compose:如何从字符串资源中显示样式化文本

17

我在我的strings.xml中有一个字符串,针对不同的语言进行本地化。每种本地化都为字符串添加了Html标签样式。

使用Android TextView,我可以通过读取字符串资源来显示带有样式的文本。

考虑到目前Jetpack Compose (1.0.0-rc02)不支持Html标签,我尝试按照官方文档中的方法,在AndroidView组合中使用TextViewhttps://developer.android.com/jetpack/compose/interop/interop-apis#views-in-compose

以下是我的尝试示例:

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

strings.xml 文件中的文本:

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

然而,使用stringResource(id = R.string.styled_text)可以提供文本但不包含Html标签。

有没有办法在Jetpack Compose中显示来自字符串资源的带有Html样式的文本?


以下两个问题类似,但它们没有从资源中读取字符串:

Jetpack Compose如何在文本中显示HTML

Android Compose:如何在Text视图中使用HTML标记

4个回答

12

正在讨论实现Jetpack Compose用户界面的问题:

https://issuetracker.google.com/issues/139320238

经过一些研究,我提出了以下解决方案,并在同一讨论中发布:

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

来源:https://issuetracker.google.com/issues/139320238#comment11

通过这些辅助方法,您可以简单地调用:

Text(annotatedStringResource(R.string.your_string_resource))

那么你的意思是我可以在“Text” Composable中使用它吗? - user17356317
是的,因为Text支持AnnotatedString,所以您可以调用在上面的答案中定义的Text(annotatedStringResource(R.string.your_string_resource)) - rewgoes
@superus8r,请你能否重新考虑一下正确答案是什么?我认为使用AndroidView并不是最好的解决方案。这里提供的答案只使用了Jetpack Compose。 - undefined

9

stringResource在幕后使用resources.getString,这会丢弃任何样式的信息。你需要创建类似于textResource的东西来获取原始值:

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

然后像这样使用:

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

是的,它可以很好地运作于旧版 Android 的 TextView 上!stringResource 似乎会完全跳过 Html 标签。 - superus8r
1
非常感谢,它起作用了!总结一下我从你的答案中理解的内容:如果资源是有样式的,使用resources.getText()会返回一个字符序列。此外,我们仍然需要使用基于视图的TextView。干得好!对于其他人,有关resources.getText()的更多信息,请参阅官方文档: https://developer.android.com/reference/android/content/res/Resources#getText(int,%20java.lang.CharSequence) - superus8r
1
请注意,这不使用主题的排版。 - rewgoes
对于需要相同的字符串资源数组,使用以下代码:@Composable @ReadOnlyComposable fun textArrayResource(@ArrayRes id: Int): Array<CharSequence> { val resources = LocalContext.current.resources return resources.getTextArray(id) } - Sava Dimitrijević
不错的解决方案。我通过.replace("\n +".toRegex(),"\n").trim()扩展了textResource。 - Daniel Alder

1
为了继续使用stringResources(),你可以按照下面的方法进行操作。 第一步 - 使用<![CDATA[ … ]]> 将所有的资源HTML字符串放在<![CDATA[ … ]]>之间,这样在调用stringResources()时就不会丢失HTML定义。
<string name="styled_text"><![CDATA[Sample text with <b>bold styling</b> to test]]></string>

第二步 - 创建一个“从SpannedAnnotatedString的处理器”

首先感谢这个很棒的答案

然后需要知道text参数(来自Text()组合函数)不仅接受字符串,还可以接受AnnotatedString对象。

继续前进...我们可以使用Jetpack的buildAnnotatedString()函数创建一个“从SpannedAnnotatedString的处理器”算法。

在这种情况下,我会选择“将算法作为客户组合文件内的一个私有函数扩展”路径:

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

第三步 - 调用`HtmlCompat.fromHtml()`。
在将`AnnotatedString`发送到`Text()`组合之前的最后一步,我们需要调用目标`String`上的`HtmlCompat.fromHtml()`方法,以及新的`toAnnotatedString()`扩展函数。
val textAsAnnotatedString = HtmlCompat.fromHtml(
    stringResource(id = R.string.styled_text),
    HtmlCompat.FROM_HTML_MODE_COMPACT
).toAnnotatedString()

第四步 - 在Text()上显示

然后只需在目标Text()组件上显示它:

Text(text = textAsAnnotatedString)

注意:你可以在toAnnotatedString()中添加很多"样式解释器"

下面的打印(红色矩形内的所有内容)来自我的Android项目中的可组合SnackBar,使用了上述相同的策略。

enter image description here


谢谢你的解决方案。@Thiengo,你知道我如何在这个编辑文本视图中为<li><ol><ul>提供支持吗? - undefined

0

目前,您可以使用AnnotatedString类来在Text组件中显示样式化文本。我将为您展示一个例子:

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

根据您分配的样式,您将获得具有红色高亮样式的文本。

https://istack.dev59.com/XSMMB.webp

更多信息请参见:https://developer.android.com/jetpack/compose/text?hl=nl#multiple-styles


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接