Android TextView的各种Span

本文详细介绍了Android中TextView的Span功能,包括SpannedString、SpannableString和SpannableStringBuilder的使用,以及如何创建和应用Span。还讨论了ForegroundColorSpan、BackgroundColorSpan、UnderlineSpan和ClickableSpan等常用Span的实现,并解决了在使用ClickableSpan时可能遇到的问题,如点击事件冲突和事件消耗问题。最后提到了如何高效地重复利用Span,避免不必要的文本复制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先Span是什么
Span是功能强大的标记对象,可用于在字符或段落级别的文本设置样式。我们可以将该标记对象添加到文本上,从而可以改变文本的颜色,使文本可点击,缩放文本大小等等。

创建Span
创建一个Span,可以使用以下三个类
SpannedString  使用该类创建的Span对象不可以改变文本,不可以改变标记。很少使用到。
SpannableString  使用该类创建的Span对象不可改变文本,可以改变标记,一般在不修改文本内容,只修改文本标记的情况下(比如修改文本的颜色等)应该使用该类。
SpannableStringBuilder  使用该类创建的Span对象可以改变文本,可以改变标记,一般在要修改文本内容时使用,如果需要将大量的Span附加到文本上,那不管是否修改文本,都应该使用该类。

应用Span
如果需要应用Span,需要对Spannable对象调用setSpan(Object _what_, int _start_, int _end_, int _flags_)。参数what代表要应用于文本的Span,start和end表示要应用该Span的文本位置(前包括后不包括,要对整个文本应用Span,start为0,end为文本长度)
在应用Span后,如果在Span边界内插入文本,则Span会自动扩展已插入的文本,如果在Span的边界上(即在start或end索引处)插入文本时。flags参数表示是否应该将Span扩展到插入的文本上。
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE 标志表示会排除插入的文本
Spannable.SPAN_EXCLUSIVE_INCLUSIVE 标志表示会包含插入的文本

常用的Span
ForegroundColorSpan
可用于改变文本颜色

val spannable = SpannableString("只要学不死,就往死里学!")
spannable.setSpan(
    ForegroundColorSpan(Color.RED),
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
textView.text = spannable

BackgroundColorSpan 可用于改变文本的背景颜色

val spannable = SpannableString("只要学不死,就往死里学!")
spannable.setSpan(
    BackgroundColorSpan(Color.RED),
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
textView.text = spannable

UnderlineSpan 可用于给文本添加下划线

val spannable = SpannableString("只要学不死,就往死里学!")
spannable.setSpan(
    UnderlineSpan(),
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
textView.text = spannable

ClickableSpan 可用于给文本添加点击事件

private val clickableSpan = object : ClickableSpan() {
    override fun onClick(widget: View) {
        Toast.makeText(this@MainActivity, "clickableSpan", Toast.LENGTH_SHORT).show()
    }

    override fun updateDrawState(ds: TextPaint) {
        //clickableSpan默认的文本有字体颜色和下划线
        //可重写此方法改变默认行为
        ds.isUnderlineText = false
    }
}
val spannable = SpannableString("只要学不死,就往死里学!")
spannable.setSpan(
    clickableSpan,
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
textView.text = spannable
//默认的clickableSpan点击后会高亮,设置textView的highlightColor属性来消除高亮
textView.highlightColor = ContextCompat.getColor(this, android.R.color.transparent)
//设置movementMethod后才有真正的有点击事件
textView.movementMethod = LinkMovementMethod.getInstance()

在使用ClickableSpan时,可能会遇到几个问题.。
一.如果在使用ClickableSpan的同时为TextView添加上点击事件,在点击Span的区域时,会同时触发两个回调事件。这是因为TextView的onTouchEvent()会首先调用super.onTouchEvent()。然后才会调用我们的mMovement.onTouchEvent()。由于View的onClick事件会被post到主线程调用。有一定的延迟。所以Span的点击事件会优先于View的onClick事件。我们可以设置一个标志位。来过滤掉View的onClick事件

//设置标志位来过滤事件
var isClickSpan = false

textView = findViewById(R.id.textView)
textView.setOnClickListener {
    //如果span的点击事件没触发
    if(!isClickSpan) {
        Log.d("TAG",  "textView")
    }
    //重置标志位
    isClickSpan = false
}

private val clickableSpan = object : ClickableSpan() {
    override fun onClick(widget: View) {
        //更改标志位
        isClickSpan = true
        Log.d("TAG", "clickableSpan")
    }
}

val spannable = SpannableString("只要学不死,就往死里学!")
spannable.setSpan(
    clickableSpan,
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)
textView.text = spannable
textView.movementMethod = LinkMovementMethod.getInstance()

二.如果为一个TextView添加上ClickableSpan,则不管点击的是不是Span区域。TextView都会默认消耗掉此次事件。这样会导致该TextView所在的ViewGroup接收不到事件回调。因为默认事件都被TextView消耗掉了。我们可以重写LinkMovementMethod的onTouchEvent()

class CustomLinkMovementMothod : LinkMovementMethod() {
    companion object {
        private var instance: CustomLinkMovementMothod? = null
            get() {
                if (field == null) {
                    field = CustomLinkMovementMothod()
                }
                return field
            }

        @JvmStatic
        fun get() : CustomLinkMovementMothod {
            return instance!!
        }
    }

    override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
        val b =  super.onTouchEvent(widget, buffer, event)
        if (!b && event?.action == MotionEvent.ACTION_UP) {
            val viewParent = widget?.parent
            if (viewParent is ViewGroup) {
                //显示的调用performClick
                //来触发onClick的回调
                viewParent.performClick()
            }
        }
        return b
    }
}

重复利用Span
当我们使用setText()重载时,TextView会创建Spannable的副本作为SpannedString,并将其作为CharSequence保存在内存中。这意味着文本和Span不可变。而当我们需要更新文本或Span时,会需要创建一个新的Spannable对象并再次调用setText()。
如果表示我们创建的Span可变。可调用setText(CharSequence text, TextView.BufferType type)

textView.setText(spannable, TextView.BufferType.SPANNABLE)
val spannableText = textView.text as Spannable
spannableText.setSpan(BackgroundColorSpan(Color.RED),
    0,
    6,
    Spannable.SPAN_EXCLUSIVE_INCLUSIVE
)

现在,当附加,分离,或重新定位Span时,TextView会自动更新来反映对文本的更改。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值