1. 背景与挑战
在传统的 Android View 系统中,我们可以直接通过 TextView.setText(Html.fromHtml(...)) 展示带颜色的 HTML 字符串。
但在 Jetpack Compose 中,Text 组件仅接受 String 或 AnnotatedString 类型。由于 Compose 的 Text 并不原生解析 HTML 标签,因此我们需要建立一套桥接方案,将 XML 中的 HTML 字符串转换为 Compose 可识别的样式对象。
2. 核心实现方案
本方案采用 “XML 资源 + Android 原生 HTML 解析 + Compose 扩展转换函数” 的路径。
2.1 资源层:定义 HTML 字符串
在 strings.xml 中,使用 CDATA 标签包裹 HTML 内容。推荐使用 \u00A0 处理多空格需求,以防止资源编译时被压缩。
XML
<string name="ai_statistic_legend">
<![CDATA[<font color="#FBC02D">%1$s</font>代表鸡蛋\u00A0\u00A0\u00A0<font color="#FF0000">%2$s</font>代表西红柿]]>
</string>
<string name="label_yellow">黄色</string>
<string name="label_red">红色</string>
2.2 转换层:Spanned 转 AnnotatedString
编写一个通用的扩展函数,遍历 HtmlCompat.fromHtml 生成的 Spanned 对象中的所有 Span(如颜色、加粗、下划线),并将其映射到 Compose 的 SpanStyle。
Kotlin
import android.text.Html
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
/**
* 将 Android 传统的 Spanned 转换成 Compose 的 AnnotatedString
*/
fun CharSequence.toAnnotatedString(): AnnotatedString {
val spanned = this as? Spanned ?: return AnnotatedString(this.toString())
return buildAnnotatedString {
append(spanned.toString())
// 1. 处理颜色 (对应 HTML 的 <font color="...">)
val colorSpans = spanned.getSpans(0, spanned.length, ForegroundColorSpan::class.java)
colorSpans.forEach { span ->
val start = spanned.getSpanStart(span)
val end = spanned.getSpanEnd(span)
addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
}
// 2. 处理样式 (对应 HTML 的 <b> <i>)
val styleSpans = spanned.getSpans(0, spanned.length, StyleSpan::class.java)
styleSpans.forEach { span ->
val start = spanned.getSpanStart(span)
val end = spanned.getSpanEnd(span)
when (span.style) {
android.graphics.Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
android.graphics.Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
}
}
// 3. 处理下划线 (对应 HTML 的 <u>)
val underlineSpans = spanned.getSpans(0, spanned.length, UnderlineSpan::class.java)
underlineSpans.forEach { span ->
val start = spanned.getSpanStart(span)
val end = spanned.getSpanEnd(span)
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
}
}
}
2.3 UI 层:组件集成
在 Composable 函数中,利用 stringResource 进行格式化注入,随后通过 HtmlCompat 进行解析。
Kotlin
@Composable
fun HtmlRichTextDisplay() {
// 获取格式化后的原始字符串 (包含 HTML 标签)
val rawHtml = stringResource(
id = R.string.ai_statistic_legend,
stringResource(R.string.label_yellow),
stringResource(R.string.label_red)
)
// 调用解析逻辑
val spanned = HtmlCompat.fromHtml(rawHtml, HtmlCompat.FROM_HTML_MODE_COMPACT)
// 展示
Text(
text = spanned.toAnnotatedString(),
color = Color.Black // 非高亮部分的默认颜色
)
}
3. 关键知识点总结
| 功能点 | 处理方案 | 备注 |
|---|---|---|
| 多语言支持 | %1$s 占位符 | 确保翻译时变量顺序可变 |
| 局部高亮颜色 | <font color="#RRGGBB"> | 需使用十六进制颜色值 |
| 多连续空格 | 使用 \u00A0 | XML 中空格会被压缩,此字符可强制保留 |
| 性能考量 | 纯 Compose Text 渲染 | 避免使用 AndroidView 嵌套 TextView |
| 扩展性 | 扩展函数适配 Span | 可根据需求添加对 <a> 或 <h1> 等标签的支持 |