文章目录

  • 一、前言
  • 二、在应用中使用标记(Span)
    • 2.1 标记(Span)的类型
    • 2.2 如何使用标记(Span)
  • 三、Android Span的类型
    • 3.1 影响文本外观的 Span
    • 3.2 影响文本指标的 Span
    • 3.3 影响单个字符的 Span
    • 3.4 影响段落的 Span
  • 四、创建自定义的 Span
  • 五、使用 Span 的最佳做法
    • 5.1 在`TextView`多次附加或分离 Span,而不更改底层文本
    • 5.2 在`TextView`中多次设置文本
    • 5.3 更改内部 Span 属性
  • 六、使用 Android KTX 扩展功能

一、前言

TextView中对显示的文本进行某些格式化的时候,很多人会首相想到使用html文本,对于简单一点的可能会用多个TextView进行拼合。然而这些都有很多的局限性,比如html文本可编辑性很差,多个TextView拼接的方式对于组合不定也是非常不方便的。Android提供了一种强大的标记对象-Span,可用于在字符或段落级别对文本设置样式。通过将 Span 附加到文本对象上,您能够以各种方式更改文本,包括添加颜色、使文本可点击、调整文本大小以及以自定义方式绘制文本。Span 还可以更改TextPaint 属性、在 Canvas 上绘制,甚至更改文本布局。

Android 提供多种类型的 Span,其中涵盖各种常见的文本样式格式。您也可以创建自己的 Span,以应用自定义样式。

喜欢阅读官方文档的也可以参考官方文档:Google官方文档入口

二、在应用中使用标记(Span)

2.1 标记(Span)的类型

在Android中,提供了几种Span,他们主要区分在于文本本身是否可变、文本标记是否可变、以及包含Span数据的底层数据结构差异,主要分为以下几种:

文本可变 标记可变 底层数据结构
SpannedString 线性数组
SpannableString 线性数组
SpannableStringBuilder 区间树

所有这些类都实现 Spanned 接口。SpannableStringSpannableStringBuilder 同时实现了 Spannable 接口。

2.2 如何使用标记(Span)

那么,该如何选择使用Span的类型呢?

  • 如果再创建后不需要对文本或者标记进行修改,使用SpannedString
  • 如果文本本身只读不变,并且需要将少量的标记动态附加到单个文本对象上,使用SpannableString
  • 如果需要在创建后修改文本,并且需要将标记动态附加到文本,使用SpannableStringBuilder
  • 如果需要将大量的标记附加到文本,无论文本本身是否只读,都是用SpannableStringBuilder

要应用 Span,先要创建一个Spannable对象,然后对 Spannable 对象调用 setSpan(Object what, int start, int end, int flags)。接口参数说明如下:

参数名 类型 说明 备注
what Object 应用于文本的Spna类型 android在android.text.style包名下定义了很多预定义的Span类型,开发者也可以自定义自己的Span
start int Span应用的开始位置 索引从0开始,标记生效于该位置的字符
end int Span应用的结束位置 标记生效到该位置前一个字符,不包括该位置的字符
flags int 标记值 该标记值在 Spanned 接口中定义,用来指定在Span边界(及在 startend索引处)处插入文本时,Span是否展开并将插入的文本包含在内。字段定义格式类似SPAN_<start>_<end>,分为 INCLUSIVE (包含) 和 EXCLUSIVE (不包含)两种,如Spanned.SPAN_EXCLUSIVE_INCLUSIVE表示在 start处插入字符,Span不会扩展包含插入的字符,插入的字符不会拥有Span样式,end处插入字符,Span将会扩展包含插入的字符,插入的字符拥有Span样式。更多详情参考对应字段声明。
  • 以下就是一个设置前景色(ForegroundColorSpan)的示例
SpannableStringBuilder(nums).also {// 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

执行的效果如下图:

  • 前面的例子是SPAN_INCLUSIVE_EXCLUSIVE,当在start出插入文本时,Span会扩展到新插入的文本
SpannableStringBuilder(nums).also {// 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)it.insert(0, "ABC")tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

执行的效果如下图:

同样可以用验证在end处插入字符,Span不会扩展到新插入的字符。

  • 同时设置多个Span
    可以使用多个Span叠加事项想要的效果,例如:前景色+加粗字体
SpannableStringBuilder(nums).also {// 这里的end是4,但是Span效果是在0~3,所以Span的结尾是不包括end所在的字符it.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)it.setSpan(StyleSpan(Typeface.BOLD), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

完整demo代码请参考:SpannableDemo 项目源码

三、Android Span的类型

Android 在 android.text.style 软件包中提供了超过 20 种 Span 类型。Android对Span的分类主要有两种方式:

  • Span 如何影响文本:Span 可能会影响文本外观(如:前景色、背景色)或文本指标(如:字号,字体)。
  • Span 作用范围:一些 Span 可应用于单个字符,还有一些 Span 必须应用于整个段落。

3.1 影响文本外观的 Span

有些Span会改变文本的外观,例如更改文本前景色或背景颜色以及添加下划线或删除线,这些Span都会扩展 CharacterStyle 类。

// 设置文本的前景色
SpannableStringBuilder(nums).also {// 这里的start是3,end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符it.setSpan(UnderlineSpan(), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

3.2 影响文本指标的 Span

Span 还会影响文本指标,例如行高和文本大小。这些 Span 都会扩展 MetricAffectingSpan 类。

// 基于原来的字体大小扩大1.5倍
SpannableStringBuilder(nums).also {// 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符it.setSpan(RelativeSizeSpan(1.5f), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

3.3 影响单个字符的 Span

有些Span 会影响字符级别的文本。例如,您可以更新背景颜色、样式或大小等。影响单个字符的 Span 会扩展 CharacterStyle 类。

// 设置文字的背景色
SpannableStringBuilder(nums).also {// 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符it.setSpan(BackgroundColorSpan(Color.GREEN), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

以下代码示例将 BackgroundColorSpan 附加到了文本中的部分字符:

3.4 影响段落的 Span

有些Span 会影响段落级别的文本,例如更改整个文本块的对齐方式或边距。这些 Span 实现 ParagraphStyle接口。

注意:使用影响段落的 Span 时,您必须将它们附加到整个段落(不包括末尾换行符)。如果您尝试将段落 Span 应用于除段落以外的其他内容,则这些 Span不会是生效。

// 设置段落文字对齐方式
SpannableStringBuilder(nums + "\n" + "34567" + "\n" + "123").also {// Span应用于整个段落it.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), 0, it.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

四、创建自定义的 Span

如果Android提供的 Span 无法满足您的需求,那么您可以实现自定义的 Span。实现自定义的 Span 时,您需要确定您的 Span 的类型(是否会影响字符或段落级别的文本,以及它是否会影响文本的布局或外观),这有助于您确定自定义的 Span 类需要扩展的基类以及可能需要实现的接口。请参考下表:

使用场景 类或接口 备注
您的 Span 会影响字符级别的文本 CharacterStyle
您的 Span 会影响段落级别的文本 ParagraphStyle 接口
您的 Span 会影响文本外观 UpdateAppearance 接口
您的 Span 会影响文本指标 UpdateLayout 接口

下面是一个简单的自定义 Span 的例子(文本扩大并且设置前景色):

/*** 自定义 Span:文本扩大并且设置前景色* 实现方案:Android已有文本扩大的 Span,所以只需要扩展文本扩大的 Span (RelativeSizeSpan)即可*/
class RelativeSizeColorSpan(relativeSize: Float, @ColorInt val color: Int): RelativeSizeSpan(relativeSize) {override fun updateDrawState(ds: TextPaint) {super.updateDrawState(ds)ds?.color = color}
}// 使用自定义 Span
SpannableStringBuilder(nums).also {// 这里的end是7,但是Span效果是在3~6,所以Span的结尾是不包括end所在的字符it.setSpan(RelativeSizeColorSpan(1.5f, Color.RED), 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)tvContent.setText(it)
}

说明:其实,大家注意到上面的例子发现,这个自定义的 Span 是将基于文本字体放大和前景色(字体颜色)两个基本 Span的组合,因为 Span 是可以组合使用的,所以可以直接使用两个 Span 组合使用来替代自定义 Span 。另外,上面的例子也可以改为继承自ForegroundColorSpan,在自定义代码中改变字体大小也可以实现一样的效果。

五、使用 Span 的最佳做法

在 TextView 中设置文本,有多种节省内存的方式,选择哪种方式取决于您的需求。

5.1 在TextView多次附加或分离 Span,而不更改底层文本

TextView.setText() 包含多种能够以不同方式处理 Span 的重载。例如,您可以使用以下代码设置 Spannable 文本对象:

textView.setText(spannableObject) // textView.text = spannableObject

当调用 setText() 的此重载方法时,TextView 会创建 Spannable 的副本作为 SpannedString,并将其作为 CharSequence 保留在内存中。这意味着您的文本和 Span 不可变,因此当您需要更新文本或 Span 时,您需要创建一个新的 Spannable 对象并再次调用 setText(),而这也会触发TextView重新测量和重新绘制布局。

如果要表明这些 Span 是可变的,您可以改为使用 setText(CharSequence text, TextView.BufferType type)这个重载方法,如下例所示:

tvContent.setText(SpannableStringBuilder(nums), TextView.BufferType.SPANNABLE)// 通过在setText()时设置的缓存类型,可以在改变了TextView内部的Span之后,不用再次调用setText()就可以更新Span显示
val s = tvContent.text as Spannable
s.setSpan(ForegroundColorSpan(Color.RED), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
s.setSpan(StyleSpan(Typeface.BOLD), 0, 4, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
// tvContent.setText(s) 这里无需再调用setText()

在上面示例中,由于 BufferType.SPANNABLE 参数,TextView 创建了 SpannableString,而由 TextView 保留的 CharSequence 对象现在具有了可变标记和不可变文本。要更新 Span,我们可以将该文本作为 Spannable 进行检索,然后根据需要更新这些 Span。

当您附加、分离或重新定位 Span 时,TextView 会自动更新以反映对文本的更改。不过请注意,如果您更改现有 Span 的内部属性,您还需要调用 invalidate()(如果进行与外观相关的更改)或 requestLayout()(如果进行与指标相关的更改)。

5.2 在TextView中多次设置文本

在某些情况下(例如使用 RecyclerView.ViewHolder)时,您可能想要重复使用 TextView 并多次设置文本。默认情况下,无论您是否设置 BufferTypeTextView 都会创建 CharSequence 对象的副本并将其保留在内存中。这样可确保所有 TextView 更新均已经过深思熟虑 - 您无法通过简单地更新原始 CharSequence 对象来更新文本。这意味着每次设置新的文本时,TextView 都会创建一个新对象。

如果您希望更好地控制此过程并避免创建额外的对象,您可以实现自己的 Spannable.Factory 并重写 newSpannable() 方法。您可以简单地对现有的 CharSequence 进行类型转换,并将其作为 Spannable 返回,而不是创建新的文本对象,如下所示:

val spannableFactory = object : Spannable.Factory() {override fun newSpannable(source: CharSequence?): Spannable {return source as Spannable}
}

注意:TextView在设置文本时,必须使用 TextView.setText(spannableObject, BufferType.SPANNABLE)。否则,源 CharSequence 将作为 Spanned 实例进行创建,并且无法转换为 Spannable,从而导致 newSpannable() 抛出 ClassCastException

在自定义Spannable.Factory并重写 newSpannable() 之后,您需要调用 TextView.setSpannableFactory() 告知 TextView 使用新的 Factory

textView.setSpannableFactory(spannableFactory)

注意:如果需要自定义TextViewSpannable.Factory,请务必在获得 TextView 的引用后立即设置 Spannable.Factory 对象。如果您使用的是 RecyclerView,请在首次扩充视图时设置 Spannable.Factory 对象。这可避免 RecyclerView 在将新的项绑定到 ViewHolder 时创建额外的对象。

5.3 更改内部 Span 属性

如果您只需更改可变 Span 的内部属性(例如改变某个 Span 的颜色),则可以通过在创建 Span 时保持对该 Span 的引用来避免多次调用 setText() 所产生的开销。当您需要修改 Span 时,您可以直接对引用进行修改,然后根据您更改的属性类型,在 TextView 上调用 invalidate()(改变外观) 或 requestLayout()(改变指标)。

val relativeSizeColorSpan = RelativeSizeColorSpan(1.5f, Color.RED)// ....override fun onClick(v: View?) {when(v?.id) {R.id.btnChangeSpanProperties1 -> {relativeSizeColorSpan.color = Color.REDtextView?.setText(SpannableStringBuilder(nums).also {// 设置 Spanit.setSpan(relativeSizeColorSpan, 3, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)}, TextView.BufferType.SPANNABLE)textView?.invalidate()}R.id.btnChangeSpanProperties2 -> {// 改变 Span的属性relativeSizeColorSpan.color = Color.GREENtextView?.invalidate()}R.id.btnChangeSpanProperties3 -> {// 改变 Span的属性relativeSizeColorSpan.color = Color.YELLOWtextView?.invalidate()}}
}

注意:内置的 Span 属性都不支持在创建之后进行更改,如果要使用这种方法,只能重写对应的 Span 类,并重写属性值覆盖。

六、使用 Android KTX 扩展功能

Android KTX 还包含扩展功能,该功能可确保能够更加轻松地与 Span 结合使用。要了解详情,请参阅有关 androidx.core.text 软件包的文档。

Android中强大的标记对象-Span相关推荐

  1. android 碎片技术,【移动开发】Android中强大的适配功能----Fragment(碎片)总结

    [移动开发]Android中强大的适配功能----Fragment(碎片)总结 发布时间:2020-06-27 00:32:58 来源:51CTO 阅读:10233 作者:zhf651555765 作 ...

  2. 【移动开发】Android中强大的适配功能----Fragment(碎片)总结

    作为大多数刚接触Android应用开发的人来说,在一个强大的Activity类中,就可以完成丰富多彩的UI工作,但是杂乱的屏幕分辨率,使得本来好不容易写好的UI,变得不堪入目...该怎么办那? 查阅了 ...

  3. android怎么根据标题解析json,如何在android中解析没有json对象标题的json数组?

    首先,我创建了解析器类JSONParser.java package com.example.myparse; import java.io.BufferedReader; import java.i ...

  4. Android中输出版权标记符号Copyright mark

    背景 版权标记图片 实现 效果图 背景 就是为了装逼,给自己开发的应用加上版权标记,例如微博的起始页面如下所示: 那我们一起来装个逼吧! 版权标记图片 没错,这就是一个圈圈里面加了一个c. 实现 在s ...

  5. 从源码角度分析Android中的Binder机制的前因后果

    为什么在Android中使用binder通信机制? 众所周知linux中的进程通信有很多种方式,比如说管道.消息队列.socket机制等.socket我们再熟悉不过了,然而其作为一款通用的接口,通信开 ...

  6. Android Intent之传递带有对象的集合(Serializable传递对象和对象集合)

    Android中Intent传递类对象提供了两种方式一种是 通过实现Serializable接口传递对象,一种是通过实现Parcelable接口传递对象. 要求被传递的对象必须实现上述2种接口中的一种 ...

  7. Android中的序列化

    序列化 一. 认识序列化 1. 序列化的定义 序列化 狭义的概念:即将对象转换为字节序列的过程 广义的概念:即将数据结构或者对象转换为我们可以存储或者传输的数据格式的一个过程 反序列化 狭义的概念:即 ...

  8. Android中使用Intent进行窗体切换,并且传值和自定义类的对象详解

    在Android中,Intent对象负责各个Activity窗口之间的切换,同时他更担负起数据传输重任. 一般情况下,使用Intent对象进行简单窗口切换的代码如下: Intent i=new Int ...

  9. android中shape资源定义,Android可绘制对象资源之shape和layer-list使用

    Code4Android.jpg 前言 文章中内容多来自谷歌官方文档详戳,一些示例代码详戳GitHub,不喜请轻喷. 可绘制对象资源 可绘制对象资源是一般概念,是指可在屏幕上绘制的图形,以及可以使用 ...

最新文章

  1. 基于wsimport生成代码的客户端
  2. win32下多线程同步方式之临界区,互斥量,事件对象,信号量
  3. js rsa验签_js rsa sign使用笔记(加密,解密,签名,验签)
  4. Xtragrd 取消当前行
  5. 解决gc current request等待事件
  6. ci中如何得到配置的url
  7. [再寄小读者之数学篇](2014-11-14 矩阵的应用: 多项式)
  8. 跟我一起学Redis之看完这篇比常人多会三种类型实战(又搞了几个小时)
  9. Mybatis中的延迟加载的使用方法
  10. tomcat 内存溢出问题
  11. python中定义的类的方法调用老出现missing 1 postional argument 或者self的解决办法
  12. 190310每日一句
  13. 差分约束——vijos1589
  14. STATA:面板数据滞后需要注意(同一家企业滞后出现空缺数据的原因)
  15. sqlite循环插入时使用stmt需要reset,否则会插入出错
  16. ibm服务器硬盘raid检测,IBM 3650 服务器做的RAID5,两块硬盘亮黄灯,但是系统正常,更换...
  17. 斐波那契数列的各种求法
  18. Photoshop-颜色的调整
  19. XStream使用方法
  20. 微信小程序之文档管理系统(含源码+论文+答辩PPT等)

热门文章

  1. 雅诗兰黛旗下高端沙龙香水品牌,祖-玛珑推出沉浸式线上香氛体验
  2. MUI如何调取相册的方法
  3. 影响中国青年的100句人生名言 4
  4. 滤波、形态学腐蚀与卷积(合集)
  5. 我报了个税,隐私就被扒光了?
  6. Python 99数乘表终端输出
  7. python-面向对象之继承
  8. 日文游戏资讯/博文/评测 翻译(一) 火箭竞技场-ロケットアリーナ
  9. 完全掌握KMP算法思想
  10. 怎么把音频的声音调大?