icode icode
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

iqqcode

保持对技术的探索实践与热爱
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基础内容

  • AndroidCore

  • Android UI

    • 布局

    • 基础控件

      • UI基础
      • Menu
      • TextView
      • Button
      • EditText
      • 加载ImageView
      • ProgressBar进度条
      • SeekBar拖动条
      • Selector背景选择器
      • 0简单UI组件
      • Menu
      • Dialog
      • CheckBox的使用
      • 扩大点击热区
      • SpannableStringBuilder
        • SpannableStringBuilder
        • Span
        • 可用的Span
          • 修改字体颜色
          • 设置背景色
          • 设置字体大小
          • 设置样式-粗体\斜体
          • 删除线
          • 下划线
          • 插入icon
          • 超链接点击事件
        • Demo实例
    • 布局优化

    • View绘制

  • Components

  • Fragment

  • 网络操作

  • 异步机制

  • 数据存储

  • 学习笔记

  • 自定义View

  • View事件体系

  • Android
  • Android UI
  • 基础控件
iqqcode
2022-12-11
目录

SpannableStringBuilder

# SpannableStringBuilder

一般来说,我们项目中展示的富文本都会封装自定义的组件,但是有时因使用起来历史包袱太重或者仅仅需要简单的高亮处理文本时,要实现这样的功能通常有下面几种实现方式:

  1. 使用多个 TextView 进行拼接显示
  2. 在 TextView 中加载 Html
  3. 使用 SpannableString / SpannableStringBuilder

第一个方案的灵活性不足,这里不考虑。

第二个方案则是使用 HTML 标签包裹字符串,然后使用 Html.fromHtml(str) 得到渲染后的字符串,设置给 TextView。这种方法,处理点击事件比较麻烦,如果想弹出本地的弹窗或者跳转到另一个Activity,可能需要通过 JS 注入的方式进行实现。

所以最常使用的方案是第三个,SpannableString 或 SpannableStringBuilder 的功能非常强大,可以让一个 TextView 变得丰富多彩,下面来具体看看。

我们可以用系统提供的SpannableString来处理,类似这样的效果,我们使用TextView就可以实现。

image-20221211173003732

SpannableStringBuilder 和 SpannableString

SpannableStirngBuilder简介:这是一个针对内容和标记都可以更改的文本的类。

https://developer.android.com/reference/android/text/SpannableStringBuilder (opens new window)

image-20221211173110355

image-20221211173452340

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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

效果如下:image-20221211194332346

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)
    }
1
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

image-20221211214737969


# 设置背景色

使用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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

效果如下:image-20221211195215754


# 设置字体大小

使用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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

效果如下:

image-20221211195227520


# 设置样式-粗体\斜体

使用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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

效果如下:

image-20221211195240421


# 删除线

使用StrikethroughSpan设置删除线。

/**
 * 字体删除线
 */
private fun textStrike() {
    val spanBuilder = SpannableStringBuilder().also {
        it.append(textStr)
        it.setSpan(
            StrikethroughSpan(),
            0,
            9,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }
    binding.spanTextView05.text = spanBuilder
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

效果如下:

image-20221211195253545


# 下划线

使用UnderlineSpan设置下划线。

/**
 * 字体下划线
 */
private fun textUnderLine() {
    val spanBuilder = SpannableStringBuilder().also {
        it.append(textStr)
        it.setSpan(
            UnderlineSpan(),
            0,
            9,
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        )
    }
    binding.spanTextView06.text = spanBuilder
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

效果如下:

image-20221211195304201


# 插入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
}
1
2
3
4
5
6
7
8
9
10
11

使用ImageSpan设置图片,将index为12、23的字符替换成了图片,也就是说,该图片占有index6和7的位置。

image-20221211195318589


# 超链接点击事件

/**
 * 字体支持点击事件
 */
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()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

效果如下:

image-20221211195334132

ClickableSpan 注意

在使用 ClickableSpan 时,需要注意以下几点:

  1. 有默认颜色和下滑线,如果想修改颜色或去掉下划线,需要重写 updateDrawState方法。
@Override public void updateDrawState(@NonNull TextPaint ds) {
    super.updateDrawState(ds);
    ds.setColor(getResources().getColor(R.color.color_black));
    ds.setUnderlineText(false);
}
1
2
3
4
5
  1. 设置 ClickableSpan 后,TextView 需要加一行

    textView.setMovementMethod(LinkMovementMethod.getInstance());,代码中指定index为5、6、7的字符都成了可点击的文本,其他区域还是不可点击的。否则ClickableSpan部分点击事件不起作用。

  2. 设置 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
    }
}
1
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

image-20221211180144892

例子中将ImageSpan、 ClickableSpan、 ForegroundColorSpan、BackgroundColorSpan 进行了组合使用


【参考文章】

[1]. 沧海一树.SpannableStringBuilder 的使用.https://juejin.cn/post/6844903966761811981

[2]. 带心情去旅行.【Android】强大的SpannableStringBuilder. https://www.jianshu.com/p/f004300c6920

编辑 (opens new window)
上次更新: 2023/10/05, 00:59:59
扩大点击热区
查看View是否显示

← 扩大点击热区 查看View是否显示→

最近更新
01
匿名内部类
10-08
02
函数式接口
10-08
03
ARouter-Kotlin踩坑
10-05
更多文章>
Theme by Vdoing | Copyright © 2021-2023 iqqcode | MIT License | 备案号-京ICP备2021028793号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×