Android中Spannable的使用

6,188 阅读4分钟

1. 简介

在安卓应用开发的过程中经常会遇到要求我们在一个文章中使用不同的style。解决这样的需求有如下三种方法。

  1. 使用多个TextView进行拼接
  2. 使用HTML
  3. 使用SpannableString

虽然说是有三种方法,但是都有各自的缺陷,我们应根据情况进行选择。

  • 使用多个TextView进行拼接的缺点是需要比单个TextView花更多的绘制时间,容易造成UI卡顿,还有灵活性不足。因为是拼接,遇到需要自动换行等问题是比较难解决。

  • 使用HTML的话,需要设计到连接点网络获取HTML或者本地读取文件,逻辑会比较复杂,且稍微耗时。而且如果需要设置点击行为,还会涉及到js注入的问题。

  • 使用SpannbleString没有明显的缺点,且使用灵活。要是鸡蛋里点骨头的话就是代码不易读。因为涉及到String长度设置的问题。

回到正题,Spannable有两个实现类,SpannableStringSpannableStringBuilder。关系如下。

SpannableStringSpannableStringBuilder 的关系和 StringStringBuilder比较相似。

2. SetSpan

SpannableStringSpannableStringBuilder 都是通过SetSpan方法来设置具体的style。 具体方法是 setSpan(Any what, int Start, int end, int flag)。 这里的Any what里传入的参数是Span类。startend 传入的是要设置style的起始和终止位置,这里需要注意的是end不包括最后一位。flag 是具体的策略,下面会讲到。

2.1 Span类

Span类的作用是告诉SpannableString要设置什么样的style。Span类有很多种,如下。

Span类 作用 备注
BackgroundColorSpan 设置文本背景颜色 参数传入一个int类型的颜色
ForegroundColorSpan 设置文本颜色 参数传入一个int类型的颜色
ClickableSpan 设置点击事件 需要继承这个类重写onClick方法
StrikethroughSpan 设置删除线效果
UnderlineSpan 设置下划线效果
AbsoluteSizeSpan 设置文字的绝对大小 第一个参数为字体大小,只有这一个参数时,单位为px,第二个参数dip,默认为false,设为true时,第一个参数size的单位是dp
RelativeSizeSpan 设置文字的相对大小
StyleSpan 设置文字粗体、斜体 Typeface.BOLD为粗体,Typeface.ITALIC为斜体, Typeface.BOLD_ITALIC为粗斜体
ImageSpan 设置图片 将[start,end)范围内的文字替换成参数传入的图片
MaskFilterSpan 修饰效果,如模糊(BlurMaskFilter)浮雕
RasterizerSpan 光栅效果
SuggestionSpan 相当于占位符
DynamicDrawableSpan 设置图片,基于文本基线或底部对齐
ScaleXSpan 基于x轴缩放
SubscriptSpan 下标(数学公式会用到)
SuperscriptSpan 上标(数学公式会用到)
TextAppearanceSpan 文本外貌(包括字体、大小、样式和颜色)
TypefaceSpan 文本字体
URLSpan 文本超链接

2.2 Flag

Flag有如下四种情况。

Flag 作用
SPAN_EXCLUSIVE_EXCLUSIVE 在文本前面或后面插入新的文本时,都不会应用该样式
SPAN_EXCLUSIVE_INCLUSIVE 在文本前插入新的文本不会应用该样式,而在文本后插入新文本会应用该样式
SPAN_INCLUSIVE_EXCLUSIVE 在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式
SPAN_INCLUSIVE_INCLUSIVE 在文本前面或后面插入新的文本时,都会应用该样式

3. SpannableString

我们用BackgroundColorSpan和UnderlineSpan作为例子,具体代码实现如下

// 实例化SpannableString, 同时把需要调整sytl的string传入
val baSpannable = SpannableString("Background and Underline")
baSpannable.setSpan(
    BackgroundColorSpan(Color.YELLOW),
    0,
    11,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
baSpannable.setSpan(
    UnderlineSpan(),
    16,
    baSpannable.toString().length,
    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
// 把SpannableString直接传入给TextView的text
// 千万不要把SpannableString转化成string再传入,style会全部失效
backgroundAndUnderline.text = baSpannable

4. SpannableStringBuilder

SpannableStringBuilder 的作用和 StringBuilder 很类似。把string传入builder中,然后进行一系列操作。 具体代码如下。

val spannableStringBuilder = SpannableStringBuilder().also {
    // 把string传入到SpannableStringBuilder中
    it.append("hello world! click here!")
    // 直接设置style
    it.setSpan(
        // 设置style的Span, 第一个参数是context, 第二个是style的int值
        TextAppearanceSpan(this, R.style.style),
        0,
        12,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    it.setSpan(
        // 设置可点击的Span,下面有讲到
        clickableSpannable,
        13,
        it.toString().length,
        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
    )
    it.setSpan(
        TextAppearanceSpan(this, R.style.clickStyle),
        13,
       it.toString().length,
       Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
        )
}
textView.text = spannableStringBuilder
// 注意: 必须要设置下面的代码, 否则没有点击效果
textView.movementMethod = LinkMovementMethod.getInstance()

5. ClickableSpan

上面代码中出现的 ClickableSpan 是可点击的Span。即点击可实现相应的点击事件。具体实现代码如下。

val clickableSpannable = object : ClickableSpan() {
    // 重写点击事件
    override fun onClick(widget: View) {
        Toast.makeText(this@MainActivity, "hello", Toast.LENGTH_LONG).show()
    }
    // 重写具体的样式, 这里只能直线颜色,下划线等等。
    override fun updateDrawState(ds: TextPaint) {
        // link的颜色
        ds.linkColor = Color.BLUE
        // 下划线的宽度
        ds.underlineThickness = 0.5F
        // 下划线有效
        ds.isUnderlineText = true
    }
}

最后把 SpannableString 赋值给 TextView的text以后需要调用下面的代码才能让点击事件有效。这个代码的具体实现原理其实就是 TouchEvent 的传递。还有需要注意的是设置 ClickableSpan ,可能会与 TextView 本身的onClick事件冲突。

// 注意: 必须要设置下面的代码, 否则没有点击效果
textView.movementMethod = LinkMovementMethod.getInstance()

github: github.com/HyejeanMOON…