Span 是强大的标记对象,可用于在
字符级别或段落级别。通过将 span 附加到文本对象,您可以更改
为文本添加颜色、使文本可点击
缩放文字大小,以及以自定义方式绘制文字。Span 还可以
更改 TextPaint
属性,在
Canvas
,然后更改文本布局。
Android 提供多种类型的 Span,涵盖各种常见的文本样式格式。您也可以创建自己的 Span,以应用自定义样式。
创建并应用 Span
如需创建 span,您可以使用下表中列出的某个类。 这些类因文本本身是否可变、文本 标记是可变的,并且哪个底层数据结构包含 span 数据。
类 | 可变文本 | 可变标记 | 数据结构 |
---|---|---|---|
SpannedString |
否 | 否 | 线性数组 |
SpannableString |
否 | 是 | 线性数组 |
SpannableStringBuilder |
是 | 是 | 区间树 |
这三个类都扩展了 Spanned
界面。SpannableString
和 SpannableStringBuilder
还会扩展 Spannable
接口。
下面介绍了如何决定使用哪一种:
- 如果您不准备在创建后修改文本或标记,请使用
SpannedString
。 - 如果您需要将少量 Span 附加到单个文本对象,并且
文本本身是只读的,请使用
SpannableString
。 - 如果您需要在创建后修改文本,并且需要将 span 附加到
文本,使用
SpannableStringBuilder
。 - 如果您需要将大量 span 附加到文本对象,无论其
请使用
SpannableStringBuilder
。
如需应用 Span,请对 Spannable
对象调用 setSpan(Object _what_, int _start_, int _end_, int
_flags_)
。what 参数是指
start 和 end 参数用于指示
文本。
如果您在 Span 的边界内插入文本,该 Span 会自动扩展为
包含插入的文字在 span 处插入文本时
边界(即 start 或 end 索引处)flags
参数确定 span 是否展开即可包含插入的文本。使用
该
Spannable.SPAN_EXCLUSIVE_INCLUSIVE
标志以包含插入的文字,并使用
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
以排除插入的文字。
以下示例展示了如何将
ForegroundColorSpan
更改为
字符串:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE )
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE );
由于此 span 是使用 Spannable.SPAN_EXCLUSIVE_INCLUSIVE
设置的,因此
扩展以包含在跨度边界处插入的文本,如
示例:
Kotlin
val spannable = SpannableStringBuilder("Text is spantastic!") spannable.setSpan( ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ) spannable.insert(12, "(& fon)")
Java
SpannableStringBuilder spannable = new SpannableStringBuilder("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, // start 12, // end Spannable.SPAN_EXCLUSIVE_INCLUSIVE ); spannable.insert(12, "(& fon)");
您可以将多个 Span 附加到同一文本。以下示例展示了如何 创建粗体和红色文本:
Kotlin
val spannable = SpannableString("Text is spantastic!") spannable.setSpan(ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan( StyleSpan(Typeface.BOLD), 8, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE )
Java
SpannableString spannable = new SpannableString("Text is spantastic!"); spannable.setSpan( new ForegroundColorSpan(Color.RED), 8, 12, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ); spannable.setSpan( new StyleSpan(Typeface.BOLD), 8, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
Android Span 类型
Android 在 android.text.style 软件包中提供了超过 20 种 Span 类型。Android 主要以两种方式对 Span 进行分类:
- Span 如何影响文本:Span 可能会影响文本外观或文本 指标。
- Span 范围:一些 Span 可应用于单个字符,而其他 Span 必须应用于整个段落。
以下部分更详细地介绍了这些类别。
影响文本外观的 Span
一些在字符级别应用的 Span 会影响文本外观,例如
更改文本或背景颜色,以及添加下划线或删除线。这些
span 会扩展
CharacterStyle
类。
以下代码示例展示了如何对下划线应用 UnderlineSpan
文本:
Kotlin
val string = SpannableString("Text with underline span") string.setSpan(UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with underline span"); string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
仅影响文本外观的 Span 会触发重新绘制文本,而不会触发重新计算布局。这些 span 用于实现
UpdateAppearance
和扩展
CharacterStyle
。
CharacterStyle
子类通过提供对文本的
更新 TextPaint
。
影响文本指标的 Span
在字符级别应用的其他 span 会影响文本指标,例如
高度和文字大小。这些跨度会扩展
MetricAffectingSpan
类。
以下代码示例创建了一个
RelativeSizeSpan
将文字大小增加 50%:
Kotlin
val string = SpannableString("Text with relative size span") string.setSpan(RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
SpannableString string = new SpannableString("Text with relative size span"); string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
应用影响文本指标的 Span 会导致观察对象 重新测量文本,以实现正确的布局和呈现。例如, 可能会导致字词分行不同。应用上述规则 span 会触发重新测量、重新计算文本布局, 文本。
影响文本指标的 Span 会扩展 MetricAffectingSpan
类,
可让子类定义 span 如何影响文本测量的抽象类
方法是提供对 TextPaint
的访问权限。由于 MetricAffectingSpan
扩展
CharacterSpan
,则子类会影响字符处文本的外观
。
影响段落的 Span
Span 还可能影响段落级别的文本,例如更改
对齐方式或文本块的外边距。影响整个段落的 Span 会实现 ParagraphStyle
。接收者
使用这些 span,您可以将其附加到整个段落,但不包括结尾
换行符。如果您尝试将段落 span 应用于
整段,Android 根本不会应用 span。
图 8 显示了 Android 如何分隔文本中的段落。
以下代码示例将
QuoteSpan
即可应用到段落。请注意,
但如果您将跨度附加到
段落,Android 根本不会应用该样式。
Kotlin
spannable.setSpan(QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
Java
spannable.setSpan(new QuoteSpan(color), 8, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
创建自定义 Span
除了现有 Android Span 中提供的功能之外,如果您还需要其他功能,可以实现自定义 Span。在实现自己的 span 时,请确定 您的 span 会影响字符级别还是段落级别的文本, 以及它会影响文本的布局或外观。这有助于您 确定您可以扩展哪些基类以及可能需要哪些接口 实施。请参考下表:
场景 | 类或接口 |
---|---|
您的 Span 会在字符级别影响文本。 | CharacterStyle |
您的 Span 会影响文本外观。 | UpdateAppearance |
您的 Span 会影响文本指标。 | UpdateLayout |
您的 Span 会在段落级别影响文本。 | ParagraphStyle |
例如,如果您需要实现可修改文本大小的自定义 span,
color,扩展 RelativeSizeSpan
。通过继承,RelativeSizeSpan
扩展 CharacterStyle
并实现两个 Update
接口。由于
类已经为 updateDrawState
和 updateMeasureState
提供了回调,
您可以替换这些回调以实现您的自定义行为。通过
以下代码会创建一个自定义 span,以扩展 RelativeSizeSpan
和
替换 updateDrawState
回调以设置 TextPaint
的颜色:
Kotlin
class RelativeSizeColorSpan( size: Float, @ColorInt private val color: Int ) : RelativeSizeSpan(size) { override fun updateDrawState(textPaint: TextPaint) { super.updateDrawState(textPaint) textPaint.color = color } }
Java
public class RelativeSizeColorSpan extends RelativeSizeSpan { private int color; public RelativeSizeColorSpan(float spanSize, int spanColor) { super(spanSize); color = spanColor; } @Override public void updateDrawState(TextPaint textPaint) { super.updateDrawState(textPaint); textPaint.setColor(color); } }
以下示例说明了如何创建自定义 span。使用同一个
对文本应用 RelativeSizeSpan
和 ForegroundColorSpan
来达到同样的效果。
测试 Span 用法
借助 Spanned
接口,您不仅可以设置 span,还可以从
文本。测试时,请实现 Android JUnit
测试,以验证是否添加了正确的 span
正确的位置。文本样式设置示例
应用
包含通过将标记附加到项目符号的 span,
BulletPointSpan
。以下代码示例展示了如何测试
项目符号是否按预期显示:
Kotlin
@Test fun textWithBulletPoints() { val result = builder.markdownToSpans("Points\n* one\n+ two") // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()) // Get all the spans attached to the SpannedString. val spans = result.getSpans<Any>(0, result.length, Any::class.java) // Check whether the correct number of spans are created. assertEquals(2, spans.size.toLong()) // Check whether the spans are instances of BulletPointSpan. val bulletSpan1 = spans[0] as BulletPointSpan val bulletSpan2 = spans[1] as BulletPointSpan // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1).toLong()) assertEquals(11, result.getSpanEnd(bulletSpan1).toLong()) assertEquals(11, result.getSpanStart(bulletSpan2).toLong()) assertEquals(14, result.getSpanEnd(bulletSpan2).toLong()) }
Java
@Test public void textWithBulletPoints() { SpannedString result = builder.markdownToSpans("Points\n* one\n+ two"); // Check whether the markup tags are removed. assertEquals("Points\none\ntwo", result.toString()); // Get all the spans attached to the SpannedString. Object[] spans = result.getSpans(0, result.length(), Object.class); // Check whether the correct number of spans are created. assertEquals(2, spans.length); // Check whether the spans are instances of BulletPointSpan. BulletPointSpan bulletSpan1 = (BulletPointSpan) spans[0]; BulletPointSpan bulletSpan2 = (BulletPointSpan) spans[1]; // Check whether the start and end indices are the expected ones. assertEquals(7, result.getSpanStart(bulletSpan1)); assertEquals(11, result.getSpanEnd(bulletSpan1)); assertEquals(11, result.getSpanStart(bulletSpan2)); assertEquals(14, result.getSpanEnd(bulletSpan2)); }
如需查看更多测试示例,请参阅 GitHub 上的 MarkdownBuilderTest。
测试自定义 span
测试 span 时,请验证 TextPaint
是否包含预期的
并在 Canvas
上显示正确的元素。假设有一个自定义 Span 实现,它在一些文本前面添加了项目符号,项目符号指定了尺寸和颜色,并且存在间隙
可绘制区域的左边缘与项目符号之间。
您可以通过实现 AndroidJUnit 测试并检查以下内容来测试此类的行为:
- 如果您正确应用 span,则会使用指定大小的项目符号和 且左边缘之间留有适当的间距 和项目符号
- 如果您未应用 Span,则不会显示任何自定义行为。
您可以在 TextStyling 中 示例。
您可以通过模拟画布并向其传递所模拟的画布来测试画布互动。
对象添加到
drawLeadingMargin()
方法,并验证是否使用正确的方法调用了正确的方法
参数。
您可以在以下位置找到更多 Span 测试示例: BulletPointSpanTest。
使用 Span 的最佳做法
您可以通过多种节省内存的方式在 TextView
中设置文本,
。
附加或分离 Span,而不更改底层文本
TextView.setText()
包含以不同方式处理 span 的多个过载。例如,您可以
使用以下代码设置 Spannable
文本对象:
Kotlin
textView.setText(spannableObject)
Java
textView.setText(spannableObject);
调用此 setText()
重载时,TextView
会创建 Spannable
的副本作为 SpannedString
,并将其作为 CharSequence
保留在内存中。这意味着您的文本和 span 是不可变的,因此当您需要
更新文本或 span,创建一个新的 Spannable
对象并调用
setText()
,这也会触发系统重新测量和
布局。
如需指明 span 必须可变,您可以改用
setText(CharSequence text, TextView.BufferType
type)
、
如以下示例中所示:
Kotlin
textView.setText(spannable, BufferType.SPANNABLE) val spannableText = textView.text as Spannable spannableText.setSpan( ForegroundColorSpan(color), 8, spannableText.length, SPAN_INCLUSIVE_INCLUSIVE )
Java
textView.setText(spannable, BufferType.SPANNABLE); Spannable spannableText = (Spannable) textView.getText(); spannableText.setSpan( new ForegroundColorSpan(color), 8, spannableText.getLength(), SPAN_INCLUSIVE_INCLUSIVE);
在此示例中,
BufferType.SPANNABLE
参数会使 TextView
创建 SpannableString
,而
由 TextView
保留的 CharSequence
对象现在具有可变标记,并且
不可更改文本。如需更新 span,请以 Spannable
形式检索文本,然后
根据需要更新 span。
当您附加、分离或重新定位 Span 时,TextView
会自动更新以反映对文本的更改。如果您更改了内部属性
现有 span 的样式,调用 invalidate()
进行与外观相关的更改,或者
requestLayout()
,进行与指标相关的更改。
在 TextView 中多次设置文本
在某些情况下(例如使用 RecyclerView.ViewHolder
时),您可能想要重复使用 TextView
并多次设置文本。修改者
无论您是否设置 BufferType
,TextView
都会创建
CharSequence
对象的副本并将其保存在内存中。这使得所有
TextView
更新是有意为之 - 您无法更新原始版本
CharSequence
对象来更新文本。也就是说,每次设置新的
文本,TextView
会创建一个新对象。
如果您希望更好地控制此过程,并避免产生额外的对象,
则可以实现自己的
Spannable.Factory
并替换
newSpannable()
。
您可以转换并返回现有的文本对象,
CharSequence
作为 Spannable
,如以下示例所示:
Kotlin
val spannableFactory = object : Spannable.Factory() { override fun newSpannable(source: CharSequence?): Spannable { return source as Spannable } }
Java
Spannable.Factory spannableFactory = new Spannable.Factory(){ @Override public Spannable newSpannable(CharSequence source) { return (Spannable) source; } };
在下列情况中,您必须使用 textView.setText(spannableObject, BufferType.SPANNABLE)
:
设置文本。否则,将创建来源 CharSequence
作为 Spanned
实例且无法转换为 Spannable
,导致 newSpannable()
抛出
ClassCastException
。
替换 newSpannable()
后,告知 TextView
使用新的 Factory
:
Kotlin
textView.setSpannableFactory(spannableFactory)
Java
textView.setSpannableFactory(spannableFactory);
设置 Spannable.Factory
对象一次,就在获得对
TextView
。如果您使用的是 RecyclerView
,请在执行以下操作时设置 Factory
对象:
首先膨胀您的视图这样可以避免在
RecyclerView
会将一个新项绑定到您的 ViewHolder
。
更改内部 Span 属性
如果您只需要更改可变 span 的内部属性,例如
采用自定义项目符号跨度显示项目符号颜色,可避免因致电而造成开销
setText()
。
需要修改 span 时,您可以修改引用,然后调用
针对 TextView
的 invalidate()
或 requestLayout()
,具体取决于
属性。
在下面的代码示例中,自定义项目符号实现具有 默认的红色,点按按钮时变为灰色:
Kotlin
class MainActivity : AppCompatActivity() { // Keeping the span as a field. val bulletSpan = BulletPointSpan(color = Color.RED) override fun onCreate(savedInstanceState: Bundle?) { ... val spannable = SpannableString("Text is spantastic") // Setting the span to the bulletSpan field. spannable.setSpan( bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) styledText.setText(spannable) button.setOnClickListener { // Change the color of the mutable span. bulletSpan.color = Color.GRAY // Color doesn't change until invalidate is called. styledText.invalidate() } } }
Java
public class MainActivity extends AppCompatActivity { private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED); @Override protected void onCreate(Bundle savedInstanceState) { ... SpannableString spannable = new SpannableString("Text is spantastic"); // Setting the span to the bulletSpan field. spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE); styledText.setText(spannable); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // Change the color of the mutable span. bulletSpan.setColor(Color.GRAY); // Color doesn't change until invalidate is called. styledText.invalidate(); } }); } }
使用 Android KTX 扩展功能
Android KTX 还包含扩展函数,以便您使用 span 。如需了解详情,请参阅有关 androidx.core.text 软件包的文档。