You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何改进Spanned转AnnotatedString函数以支持换行符\n?

改进Spanned转AnnotatedString以支持换行符

以下是一段将Android原生Spanned转换为Jetpack ComposeAnnotatedString的代码,可保留粗体、斜体、下划线和文本颜色格式:

/**
 * 将[Spanned]转换为[AnnotatedString],尽可能保留原有格式。
 * 目前支持`粗体`、`斜体`、`下划线`和`颜色`格式。
 */
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 UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
            is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
        }
    }
}

当前代码虽能保留多种格式,但无法针对性处理文本中的\n换行符,以下是改进方案:

改进后代码

/**
 * 将[Spanned]转换为[AnnotatedString],保留格式并支持`\n`换行。
 * 支持`粗体`、`斜体`、`下划线`、`颜色`格式,以及识别换行符实现换行。
 */
fun Spanned.toAnnotatedStringWithLineBreaks(): AnnotatedString = buildAnnotatedString {
    val text = this@toAnnotatedString.toString()
    // 提前缓存所有样式及对应的文本范围
    val spanRangeMap = getSpans(0, length, Any::class.java).associateWith {
        IntRange(getSpanStart(it), getSpanEnd(it))
    }

    var currentTextOffset = 0
    text.splitToSequence('\n').forEachIndexed { lineIndex, lineContent ->
        // 非首行先添加换行
        if (lineIndex > 0) {
            appendLine()
            currentTextOffset += 1 // 计入换行符的长度
        }
        // 添加当前行文本
        append(lineContent)
        // 为当前行匹配并应用样式
        spanRangeMap.forEach { (span, originalRange) ->
            val spanStart = originalRange.first
            val spanEnd = originalRange.last
            // 计算样式在当前AnnotatedString中的实际位置
            val adjustedStart = maxOf(currentTextOffset, spanStart) - currentTextOffset + (length - lineContent.length)
            val adjustedEnd = minOf(currentTextOffset + lineContent.length, spanEnd) - currentTextOffset + (length - lineContent.length)
            
            if (adjustedStart < adjustedEnd) {
                when (span) {
                    is StyleSpan -> when (span.style) {
                        Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), adjustedStart, adjustedEnd)
                        Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), adjustedStart, adjustedEnd)
                        Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), adjustedStart, adjustedEnd)
                    }
                    is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), adjustedStart, adjustedEnd)
                    is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), adjustedStart, adjustedEnd)
                }
            }
        }
        currentTextOffset += lineContent.length
    }
}

改进说明

  1. 逐行处理文本:将原文本按\n分割为多行,逐段添加到AnnotatedString,非首行前插入换行符。
  2. 样式索引校准:由于文本被分割,重新计算原Spanned中样式的对应位置,避免样式错位。
  3. 兼容原有格式:完整保留了原函数对粗体、斜体、下划线和颜色的处理逻辑。

使用该函数转换后的AnnotatedString,在Compose的Text组件中可正确识别\n实现换行,同时保留所有原有格式。

内容的提问来源于stack exchange,提问作者Сергей Беляков

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 18:59:51