SpannableStringBuilder
# SpannableStringBuilder
一般来说,我们项目中展示的富文本都会封装自定义的组件,但是有时因使用起来历史包袱太重或者仅仅需要简单的高亮处理文本时,要实现这样的功能通常有下面几种实现方式:
- 使用多个 TextView 进行拼接显示
- 在 TextView 中加载 Html
- 使用 SpannableString / SpannableStringBuilder
第一个方案的灵活性不足,这里不考虑。
第二个方案则是使用 HTML 标签包裹字符串,然后使用 Html.fromHtml(str)
得到渲染后的字符串,设置给 TextView。这种方法,处理点击事件比较麻烦,如果想弹出本地的弹窗或者跳转到另一个Activity,可能需要通过 JS 注入的方式进行实现。
所以最常使用的方案是第三个,SpannableString 或 SpannableStringBuilder 的功能非常强大,可以让一个 TextView 变得丰富多彩,下面来具体看看。
我们可以用系统提供的SpannableString来处理,类似这样的效果,我们使用TextView就可以实现。
SpannableStringBuilder 和 SpannableString
SpannableStirngBuilder简介:这是一个针对内容和标记都可以更改的文本的类。
https://developer.android.com/reference/android/text/SpannableStringBuilder (opens new window)
SpannableStringBuilder 和 SpannableString 都可以用来显示富文本,它们的关系就像 StringBuilder 和 String 的关系一样,SpannableStringBuilder 可以拼接字符串,SpannableString 不可以。它们都实现了 Spannable接口。
# Span
SpannableStringBuilder
和SpannableString
主要通过使用setSpan(Object what, int start, int end, int flags)
改变文本样式。
对应的参数含义
what
: 对应的各种Span,不同的Span对应不同的样式(后面展开说)
start
: 指定Span的开始位置
end
: 指定Span的结束位置,并不包括这个位置
flags
:取值有如下四个
Spannable. SPAN_INCLUSIVE_EXCLUSIVE
:前面包括,后面不包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本不会应用该样式Spannable. SPAN_INCLUSIVE_INCLUSIVE
:前面包括,后面包括,即在文本前插入新的文本会应用该样式,而在文本后插入新文本也会应用该样式Spannable. SPAN_EXCLUSIVE_EXCLUSIVE
:前面不包括,后面不包括Spannable. SPAN_EXCLUSIVE_INCLUSIVE
:前面不包括,后面包括
# 可用的Span
下面表格中列出部分可用的 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 | 文本超链接 |
# 修改字体颜色
创建SpannableString
的时候,传入需要显示的字符串。使用ForegroundColorSpan
为文字设置颜色。
可以看出SpannableStringBuilder
的可拼接性,这里同样采用了ForegroundColorSpan
为文本设置颜色。
/**
* 字体颜色
*/
private fun textColor() {
val str01 = "君不见黄河之水天上来 "
val str02 = "奔流到海不复回"
val spanBuilder = SpannableStringBuilder().also {
// SpannableStringBuilder的可拼接性
it.append(str01)
it.append(str02)
it.setSpan(
ForegroundColorSpan(Color.parseColor("#b1f8c1")),
0,
10,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.spanTextView01.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
效果如下:
Spanned中flags的标记,是在SpannableStringBuilder中使用的,在SpannableString中没有作用。
【使用场景】
使用了SpannableStringBuilder.insert(int,CharSeque)方法后,对于insert后的字符串是否进行扩展特性的标记, 此标记作用的场景仅仅是insert的位置恰好处于start 或者 end两个端点的临界位置;即用flags标记这个临界点跟随哪个。
private fun foregroundColorSpan() {
val textView01 = TextView(this)
var spanBuilder = SpannableStringBuilder("手续费84.00元")
val span1 = ForegroundColorSpan(Color.RED)
spanBuilder.setSpan(span1, 3, 8, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
textView01.text = spanBuilder
binding.spanContainer.addView(textView01)
// 插入的9扩展了特性,而插入的5未扩展特性
spanBuilder.insert(3, "9").insert(9, "5")
val textView02 = TextView(this)
textView02.text = spanBuilder
binding.spanContainer.addView(textView02)
// 插入的9未扩展特性,而插入的5扩展了特性
spanBuilder.insert(3, "9").insert(9, "5")
val textView03 = TextView(this)
textView03.text = spanBuilder
binding.spanContainer.addView(textView03)
// 移除指定span
spanBuilder.removeSpan(span1)
val textView04 = TextView(this)
textView04.text = spanBuilder
binding.spanContainer.addView(textView04)
// 恢复SpannableStringBuilder
spanBuilder = SpannableStringBuilder("手续费84.00元")
spanBuilder.setSpan(span1, 3, 8, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
// 验证同样的情况,只要执行insert系列方法,,即使flags改变效果也不变
val textView05 = TextView(this)
textView05.text = spanBuilder
binding.spanContainer.addView(textView05)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 设置背景色
使用BackgroundColorSpan
设置背景颜色。
/**
* 字体背景色
*/
private fun textBackGroundColor() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
it.setSpan(
BackgroundColorSpan(Color.parseColor("#f8b1c4")),
6,
14,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.spanTextView02.text = spanBuilder
2
3
4
5
6
7
8
9
10
11
12
13
14
效果如下:
# 设置字体大小
使用AbsoluteSizeSpan
设置字体大小。
/**
* 字体大小
*/
private fun textSize() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
it.setSpan(
AbsoluteSizeSpan(20),
0,
9,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.spanTextView03.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
效果如下:
# 设置样式-粗体\斜体
使用StyleSpan
设置粗体\斜体,多次使用setSpan
并不影响。
/**
* 字体样式
*/
private fun textStyle() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
// setSpan可多次使用
val styleSpan = StyleSpan(Typeface.BOLD) //粗体
it.setSpan(styleSpan, 0, 3, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
val styleSpan2 = StyleSpan(Typeface.ITALIC) // 斜体
it.setSpan(styleSpan2, 3, 6, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
val styleSpan3 = StyleSpan(Typeface.BOLD_ITALIC) // 粗斜体
it.setSpan(styleSpan3, 6, 9, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
}
binding.spanTextView04.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
效果如下:
# 删除线
使用StrikethroughSpan
设置删除线。
/**
* 字体删除线
*/
private fun textStrike() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
it.setSpan(
StrikethroughSpan(),
0,
9,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.spanTextView05.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
效果如下:
# 下划线
使用UnderlineSpan
设置下划线。
/**
* 字体下划线
*/
private fun textUnderLine() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
it.setSpan(
UnderlineSpan(),
0,
9,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
binding.spanTextView06.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
效果如下:
# 插入icon
不仅支持文字样式,还可以插入图片icon
/**
* 字体插入图标(图标占去该字符位置)
*/
private fun textWithIcon() {
val spanBuilder = SpannableStringBuilder().also {
it.append(textStr)
val imageSpan = getDrawable()?.let { it1 -> ImageSpan(it1) }
it.setSpan(imageSpan, 12, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
binding.spanTextView07.text = spanBuilder
}
2
3
4
5
6
7
8
9
10
11
使用ImageSpan设置图片,将index为12、23的字符替换成了图片,也就是说,该图片占有index6和7的位置。
# 超链接点击事件
/**
* 字体支持点击事件
*/
private fun textClickable() {
val spanBuilder = SpannableStringBuilder().also {
it.append("君不见黄河之水天上来")
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
Toast.makeText(this@SpanActivity, "点击字体超链接", Toast.LENGTH_SHORT).show(
}
}
it.setSpan(clickableSpan, 5, 8, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
}
binding.spanTextView08.text = spanBuilder
binding.spanTextView08.movementMethod = LinkMovementMethod.getInstance()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
效果如下:
ClickableSpan 注意
在使用 ClickableSpan 时,需要注意以下几点:
- 有默认颜色和下滑线,如果想修改颜色或去掉下划线,需要重写 updateDrawState方法。
@Override public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(getResources().getColor(R.color.color_black));
ds.setUnderlineText(false);
}
2
3
4
5
设置 ClickableSpan 后,TextView 需要加一行
textView.setMovementMethod(LinkMovementMethod.getInstance());
,代码中指定index为5、6、7的字符都成了可点击的文本,其他区域还是不可点击的。否则ClickableSpan部分点击事件不起作用。设置 ClickableSpan,可能会与 TextView 本身的onClick事件有冲突。
# Demo实例
class SpanActivity : AppCompatActivity() {
private lateinit var binding: ActivitySpanBinding
private val textStr = "君不见黄河之水天上来,奔流到海不复回"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySpanBinding.inflate(layoutInflater)
setContentView(binding.root)
textStyle()
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun textStyle() {
val builder = SpannableStringBuilder().also {
it.append(textStr)
it.setSpan(ForegroundColorSpan(Color.BLUE), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(BackgroundColorSpan(Color.RED), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
Toast.makeText(this@SpanActivity, "听风吹雨", Toast.LENGTH_SHORT).show()
UtilHelper.dealLink("http://www.baidu.com", this@SpanActivity)
}
}, 3, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(StrikethroughSpan(), 8, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(UnderlineSpan(), 9, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(AbsoluteSizeSpan(50), 10, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
it.setSpan(StyleSpan(Typeface.BOLD), 11, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
val imageSpan = getDrawable()?.let { it1 -> ImageSpan(it1) }
it.setSpan(imageSpan, 12, 13, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
binding.spanTextView01.text = builder
binding.spanTextView01.movementMethod = LinkMovementMethod.getInstance()
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun getDrawable(): Drawable? {
val drawable = this.getDrawable(R.drawable.happy_emjio)
val displayMetrics = DisplayMetrics();
this.windowManager.defaultDisplay.getMetrics(displayMetrics);
val size = 90 * displayMetrics.heightPixels / 2560;
drawable?.setBounds(0, 0, size, size);
return drawable
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
例子中将ImageSpan
、 ClickableSpan
、 ForegroundColorSpan
、BackgroundColorSpan
进行了组合使用
【参考文章】
[1]. 沧海一树.SpannableStringBuilder 的使用.https://juejin.cn/post/6844903966761811981
[2]. 带心情去旅行.【Android】强大的SpannableStringBuilder. https://www.jianshu.com/p/f004300c6920