Android 文字高亮可点击

598 阅读3分钟

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

通过SpannableStringBuilder,实现TextView的部分文本高亮可点击

实现

inner class ClickSpan : ClickableSpan() {
    override fun onClick(widget: View) {
        // 点击事件
        Toast.makeText(requireContext(), "点击", Toast.LENGTH_LONG).show()
    }

    override fun updateDrawState(ds: TextPaint) {
        super.updateDrawState(ds)
        // 富文本颜色
        ds.color = ContextCompat.getColor(requireContext(), R.color.purple_200)
        // 默认有下划线
        ds.isUnderlineText = false
    }
}

textView.movementMethod = LinkMovementMethod.getInstance()

val str =
    "如今互联网提供各种各样版本的Lorem Ipsum段落,但是大多数都多多少少出于刻意幽默或者其他随机插入的荒谬单词而被篡改过了。如果你想取用一段Lorem Ipsum,请确保段落中不含有令人尴尬的不恰当内容。所有网上的Lorem Ipsum生成器都倾向于在必要时重复预先准备的部分,然而这个生成器则是互联网上首个确切的生成器。它使用由超过200个拉丁单词所构造的词典,结合了几个模范句子结构,来生成看起来恰当的Lorem Ipsum。因此,生成出的结果无一例外免于重复,刻意的幽默,以及非典型的词汇等等。"
  1. 指定高亮文本的范围
textView.text = SpannableStringBuilder(str).apply {
    setSpan( richTextClickSpan, 2, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE  )
}

截屏2022-09-09 23.25.21.png

  1. 指定文本内容高亮
val raw = "Lorem Ipsum"
val result = SpannableStringBuilder(str)
for (matchResult in raw.toRegex().findAll(str)) {
    result.setSpan(
        ClickSpan(),
        matchResult.range.first,
        matchResult.range.last + 1,
        Spanned.SPAN_INCLUSIVE_EXCLUSIVE
    )
}

截屏2022-09-09 23.27.44.png

SpannableStringBuilder

public class SpannableStringBuilder implements CharSequence, GetChars, Spannable, Editable, Appendable, GraphicsOperations 

SpannableStringBuilder

  1. 实现CharSequence接口:和String一样,表示文本内容;
  2. 实现Appendable接口:和StringBuilder一样,可以用append方法添加文本
  3. 实现Editable接口:可以用replace方法替换文本的部分内容

setSpan方法可以设置富文本样式

public void setSpan(Object what, int start, int end, int flags) {
    setSpan(true, what, start, end, flags, true/*enforceParagraph*/);
}

Spanned区间的INCLUSIVE/EXCLUSIVE

参数flags有四种值, 表示start-end的区间的开闭情况,未使用insert方法时不生效,参数flags语义是要不要包含在start-end的区间之前和之后insert方法加入的文本

  • Spanned.SPAN_INCLUSIVE_INCLUSIVE,包括start和end
  • Spanned.SPAN_INCLUSIVE_EXCLUSIVE,包括start不包括end
  • Spanned.SPAN_EXCLUSIVE_INCLUSIVE,不包括start包括end
  • Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,不包括start和end
// 以下的flags参数没生效,会固定表现为 Spanned.SPAN_INCLUSIVE_EXCLUSIVE
// SPAN_INCLUSIVE_INCLUSIVE
val result1 = SpannableStringBuilder("如今互联网提供各种各样版本")
result1.setSpan(ClickSpan(), 0, 5, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
binding.textview1.text = result1
// SPAN_INCLUSIVE_EXCLUSIVE
val result2 = SpannableStringBuilder("如今互联网提供各种各样版本")
result2.setSpan(ClickSpan(), 0, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
binding.textview2.text = result2
// SPAN_EXCLUSIVE_INCLUSIVE
val result3 = SpannableStringBuilder("如今互联网提供各种各样版本")
result3.setSpan(ClickSpan(), 0, 5, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
binding.textview3.text = result3
// SPAN_EXCLUSIVE_EXCLUSIVE
val result4 = SpannableStringBuilder("如今互联网提供各种各样版本")
result4.setSpan(ClickSpan(), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.textview4.text = result4

image.png

如果是Spanned.SPAN_INCLUSIVE_INCLUSIVE,则在末尾插入的文本也会被设为富文本,而Spanned.SPAN_INCLUSIVE_EXCLUSIVE,则不会

val result1 = SpannableStringBuilder("如今互联网提供各种各样版本")
result1.setSpan(ClickSpan(), 2, 5, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
result1.insert(5, "insert")
binding.textview1.text = result1

val result2 = SpannableStringBuilder("如今互联网提供各种各样版本")
result2.setSpan(ClickSpan(), 2, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
result2.insert(5, "insert")
binding.textview2.text = result2

image.png

stackoverflow上有一条10年前的提问,讨论了这个问题

setSpan

每个文本可以调用多次setSpan

val bgSpan = BackgroundColorSpan(ContextCompat.getColor(requireContext(), R.color.black))
val clickSpan = ClickSpan()
val result1 = SpannableStringBuilder("如今互联网提供各种各样版本")
result1.setSpan(clickSpan, 2, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
result1.setSpan(bgSpan, 2, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
binding.textview1.text = result1

image.png

每个文本中,同一个span对象只能用一次;使用多次的话,只有最后一次生效,setSpan会保存每个span对象的区间,之前设置的会被覆盖

val clickSpan = ClickSpan()
val result1 = SpannableStringBuilder("如今互联网提供各种各样版本")
result1.setSpan(clickSpan, 2, 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
result1.setSpan(clickSpan, 8, 10, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
binding.textview1.text = result1

image.png