You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Jetpack Compose中HTML文本渲染异常与超链接点击失效问题求助

Jetpack Compose中HTML文本渲染异常与超链接点击失效问题求助

大家好,我最近在Jetpack Compose项目里实现一个可折叠的Accordion组件,需要渲染带HTML格式的文本,还要支持超链接点击跳转,但遇到了两个头疼的问题,折腾半天没解决,来向大家求助!

遇到的问题

  1. HTML格式的文本完全没有被解析,渲染出来就是纯文本,像加粗、斜体、列表项这些样式完全不生效;
  2. 文本里的超链接点击没有反应,触发不了设置好的onLinkClick回调。

我的实现代码

首先是主组件AccordionView

@Composable
fun AccordionView(
    titleHtml: String,
    descriptionHtml: String,
    onLinkClick: (String) -> Unit = {}
) {
    var expanded by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(if (expanded) 180f else 0f, label = "arrowRotation")

    Column(modifier = Modifier.padding(12.dp)) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .clickable { expanded = !expanded },
            verticalAlignment = Alignment.CenterVertically
        ) {
            HtmlText(titleHtml, onLinkClick = onLinkClick)
            Icon(
                painter = painterResource(id = android.R.drawable.arrow_down_float),
                contentDescription = null,
                modifier = Modifier.rotate(rotation)
            )
        }
        AnimatedVisibility(expanded) {
            HtmlText(descriptionHtml, onLinkClick = onLinkClick)
        }
    }
}

然后是负责渲染HTML的HtmlText组件:

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.core.text.HtmlCompat

@Composable
fun HtmlText(
    html: String,
    onLinkClick: (String) -> Unit = {}
) {
    val spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
    val annotated = spannedToAnnotatedString(spanned)

    SelectionContainer { // 允许文本选中
        Text(
            text = annotated,
            style = MaterialTheme.typography.bodyMedium,
            modifier = Modifier.pointerInput(Unit) {
                detectTapGestures { offset ->
                    annotated.getStringAnnotations("URL", offset, offset)
                        .firstOrNull()?.let { onLinkClick(it.item) }
                }
            }
        )
    }
}

最后是把Spanned转成Compose的AnnotatedString的工具函数:

import android.text.Spanned
import android.text.style.StyleSpan
import android.text.style.URLSpan
import android.text.style.BulletSpan
import androidx.compose.material3.MaterialTheme
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
import androidx.core.text.HtmlCompat

fun spannedToAnnotatedString(spanned: Spanned): AnnotatedString {
    return buildAnnotatedString {
        append(spanned.toString())

        // 处理加粗/斜体
        spanned.getSpans(0, spanned.length, StyleSpan::class.java).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
                )
            }
        }

        // 处理链接
        spanned.getSpans(0, spanned.length, URLSpan::class.java).forEach { span ->
            val start = spanned.getSpanStart(span)
            val end = spanned.getSpanEnd(span)
            addStyle(
                SpanStyle(
                    color = MaterialTheme.colorScheme.primary,
                    textDecoration = TextDecoration.Underline
                ),
                start,
                end
            )
            addStringAnnotation("URL", span.url, start, end)
        }

        // 处理列表项
        spanned.getSpans(0, spanned.length, BulletSpan::class.java).forEach { span ->
            val start = spanned.getSpanStart(span)
            insert(start, "• ")
        }
    }
}

自己的排查思路

我原本的想法是先把HTML转成Android原生的Spanned,再转成Compose的AnnotatedString,然后通过addStyleaddStringAnnotation来应用样式和标记链接,最后用pointerInput检测点击位置来触发链接跳转,但实际运行完全达不到预期。

有没有大佬能帮我看看哪里出问题了?或者有没有更正确的实现方式?感谢大家!

火山引擎 最新活动