SpannableString是 Android 中的 富文本处理类,它允许在字符串的不同部分应用不同的样式(颜色、字体、点击事件、图片等)。
相比SpannableStringBuilderSpannableString是不可变的,创建后不能修改内容,但可以添加Span(样式)。

二者区别感觉可以类比 String 和 StringBuilder

常用 Span 类型

- **文本样式**:`StyleSpan`(粗体、斜体)、`TypefaceSpan`(字体)。
- **颜色与背景**:`ForegroundColorSpan`(文字颜色)、`BackgroundColorSpan`。
- **交互功能**:`ClickableSpan`(点击事件)、`URLSpan`(超链接)。
- **布局调整**:`RelativeSizeSpan`(字体缩放)、`UnderlineSpan`(下划线)。

应用场景

文本高亮在搜索功能中,将关键词标记为特殊颜色(示例):

text.setSpan(BackgroundColorSpan(Color.YELLOW), keywordStart, keywordEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

混合样式文本实现同一行文字中部分加粗、部分变色,如“错误:数据加载失败”中的“错误”为红色加粗。

可点击文本结合ClickableSpan实现超链接或按钮功能:

text.setSpan(object : ClickableSpan() {
    override fun onClick(widget: View) { /* 处理点击 */ }
}, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

动态文本拼接使用SpannableStringBuilder(可变版本)灵活组合多个 Span,例如聊天界面中混合用户名、内容和表情。

使用注意事项

  1. 性能优化避免在长文本中频繁添加 Span(如列表项),建议复用 Spannable 对象或使用 SpannableStringBuilder
  2. 索引范围startend 参数需严格符合字符串的索引区间(从 0 开始),否则会抛出异常。
  3. 交互设置
    • ClickableSpan需为TextView设置movementMethod = LinkMovementMethod.getInstance()
    • 避免与TextView的默认点击事件冲突。

与 SpannableStringBuilder 的区别

特性SpannableStringSpannableStringBuilder
可变性不可变可变(支持追加、插入)
适用场景单次设置固定文本动态构建复杂文本
性能较高较低(频繁修改时)

项目中的应用

通过SpannableStringBuilder构造标签分隔符|

主要逻辑:

  1. SpannableStringBuilder用于构造富文本
  2. 遍历tags
    • 追加 tag 文字
    • 如果不是最后一个标签,则插入 特殊符号\uFFFC作为占位符,并 添加图片Span
  3. 最后转换为SpannableString并返回。
    **(1) **getTagsSpan方法解析
private fun getTagsSpan(tags: List<String>): SpannableString {
    val spanTmp = SpannableStringBuilder()
    val suffix = "\uFFFC"  // 特殊字符(占位符)
 
    // 计算图片高度(稍小于文字)
    val imageHeight = getTextHeight(DeviceUtils.ScreenInfo.dp2px(context, TAG_FONT_SIZE).toFloat()).toInt() - DIFactory.dp2px(2f)
 
    // 遍历 tags,每个 tag 之间插入图片
    tags.forEachIndexed { index, tag ->
        spanTmp.append(tag)  // 添加文本
 
        if (index < tags.size - 1) {  // 不是最后一个 tag
            spanTmp.append(suffix)   // 添加占位符
            spanTmp.addImageSpan(start = spanTmp.length - 1, imageHeight = imageHeight)  // 替换成图片
        }
    }
 
    return SpannableString(spanTmp) // 返回 SpannableString
}

🔹 作用

  • 遍历tags列表,依次添加tag文本。
  • 如果不是最后一个tag,就插入一个占位符\uFFFCImageSpan替换,形成“文本-图片-文本”的效果。
  • 最后返回SpannableString保证不可变性

**(2) **addImageSpan方法解析

private fun SpannableStringBuilder.addImageSpan(
    start: Int,
    imageHeight: Int? = null,
    end: Int? = null,
    flags: Int = Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
) {
    // 获取 drawable 资源
    val drawable = getVideoScaledDrawableRes(com.baidu.searchbox.video.feedflow.component.R.drawable.video_flow_collection_panel_item_no_select_landscape_new) ?: return
 
    // 计算 drawable 的高度
    val drawableHeight = imageHeight ?: drawable.intrinsicHeight
    drawable.setBounds(0, 0, drawable.intrinsicWidth, drawableHeight)
 
    // 创建 ImageSpan
    val imageSpan = SummaryEnrichedSpanHelper.VerticalImageSpan(drawable)
 
    // 将图片 Span 替换到字符串中
    setSpan(imageSpan, start, end ?: (start + 1), flags)
}

🔹 作用

  • start位置的\uFFFC占位符替换为图片ImageSpan)。
  • setBounds()设置Drawable的大小,保证与文本匹配。

**(3) **getTextHeight方法解析

private fun getTextHeight(textSize: Float): Float {
    val paint = Paint()
    paint.textSize = textSize
    val fontMetrics = paint.fontMetrics
    return fontMetrics.descent - fontMetrics.ascent
}

🔹 作用:计算字体高度,用于 调整图片高度,使得图片和文字对齐。

代码整体解析

  • getTagsSpan(tags: List<String>)创建SpannableString,在tags之间插入ImageSpan
  • addImageSpan()\uFFFC** 替换成**ImageSpan,使文字与图片混排。
  • getTextHeight()计算字体高度,使得图片与文本对齐

解析带 emoji 的 subTitle

更新: 2026-03-15 17:39:30
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/fvbllqtabo4r9mtr


相关笔记