Jetpack Compose 中处理与展示 HTML 富文本

29 阅读2分钟

1. 背景与挑战

在传统的 Android View 系统中,我们可以直接通过 TextView.setText(Html.fromHtml(...)) 展示带颜色的 HTML 字符串。

但在 Jetpack Compose 中,Text 组件仅接受 StringAnnotatedString 类型。由于 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 转换层:SpannedAnnotatedString

编写一个通用的扩展函数,遍历 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">需使用十六进制颜色值
多连续空格使用 \u00A0XML 中空格会被压缩,此字符可强制保留
性能考量纯 Compose Text 渲染避免使用 AndroidView 嵌套 TextView
扩展性扩展函数适配 Span可根据需求添加对 <a><h1> 等标签的支持