Span 是什么
看上图中的 Span 是什么?如果你回答是 测试 或者 测试 及下面的下划线那就错了。我们看一下 Google 的定义
Span 是功能强大的标记对象,可用于在字符或段落级别为文本设置样式。通过将 Span 附加到文本对象,能够以各种方式更改文本,包括添加颜色、使文本可点击、缩放文本大小以及以自定义方式绘制文本。Span 还可以更改 TextPaint 属性、在 Canvas 上绘制,以及更改文本布局。
简单的说,Span 是用来处理指定范围内的文本样式的工具。如上图,Span 就为[0, 1]范围内的文本设置了下划线的样式。这里是为了告诉你 Span 不是文本,它是文本处理的工具。真正的文本是分别实现 Spannable 和 Spanned 接口的三个类,分别是 SpannedString 、SpannableString 、SpannableStringBuilder 。这三个实现类稍后再讲,我们先看一下 Spannable 和 Spanned 接口。
Spannable 和 Spanned 接口
Spannable 和 Spanned 的区别很简单,Spanned 只能获取 Span,但 Spannable 可以设置和修改 Span 。Spannable 继承 Spanned ,并增加了 setSpan 、removeSpan 方法来设置和修改 Span 。我们先来看 Spannable 接口定义的方法。
Spannable 接口方法
public void setSpan(Object what, int start, int end, int flags);
setSpan 方法为文本设置 Span ,参数作用如下:
- what : 为文本设置的 Span
- start : 设置 Span 的开始的位置
- end : 设置 Span 的结束的位置,end 参数值是不被包含的。例如,要设置上图的下滑线效果,end 值要设置为 2 ,而不是 1。
- flags : Span 的标志位
这里 start 可以等于 end 吗?答案是 SpannedString SpannableString 可以;而 SpannableStringBuilder 在 flags 为 SPAN_EXCLUSIVE_EXCLUSIVE 时不行。flags 的作用到底是什么?这里先不讲,等后面详细介绍 flags 时,你就知道了。
还有个点需要注意,setSpan 内部会判断 what 对象是否之前设置过了,如果是同一个对象,会修改它的位置和flag值。示例代码如下:
UnderlineSpan underlineSpan = new UnderlineSpan();
spannableString.setSpan(underlineSpan, 0, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
spannableString.setSpan(underlineSpan, 2, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
spannableString.setSpan(new UnderlineSpan(), 0, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new UnderlineSpan(), 0, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
public void removeSpan(Object what) 方法用来删除指定的 Span 。这个很简单,就不多介绍了。
Spanned 接口方法
public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind)
getSpans 方法获取指定“范围”的指定 Class 的 Span 。如果你想要获取所有的 Span ,你可以使用 Object.class。这里的范围要特别注意,一般我们说的范围是指 start < pos < end 的部分,如下图。 但是,getSpan 方法不光会获取 start < pos < end 的部分,还会获取部分位置在范围内的 Span 。如下图所示: 为什么 Android 要使用这种规则来获取 Span 呢?
答案是让一个 SpanWatcher 能监听到多个 Span。SpanWatcher 是监听 Span (add、remove、set操作时)变化的 Span。它的原理很简单,就是当 Span 变化时,会调用 getSpan 获取范围内的 SpanWatcher,然后调用相应的方法。看下图,采用当前的规则下,SpanWatcher 能监听到 Span1、Span2、Span3; SpanWatcher1 能监听 Span1、Span2; SpanWatcher2 能监听 Span2。如果采用start < pos < end 的部分的规则,就无法通过一个 SpanWatcher 监听多个 Span 了。
public int nextSpanTransition(int start, int limit, Class kind)
返回一个类型的 Span 开始或结束的第一个大于start的偏移,如果没有大于start但小于limit的开始或结束,则返回limit。看文字有点难理解,直接上图。 除了图上的两种情况外,nextSpanTransition 方法只会返回传入的 limit 的值。你没看错,即使 SpanStart == start 时也是返回 limit 的值。 其他 Spanned 定义的方法的介绍如下:
public int getSpanStart(Object what) : 获取指定 Span 的 start 的值。没找到返回 -1public int getSpanEnd(Object what) : 获取指定 Span 的 end 的值。没找到返回 -1public int getSpanFlags(Object tag) : 获取 Span 的 flag。没有则返回 0
SpannedString 、SpannableString 和 SpannableStringBuilder
对于这三个类,直接看下面官方文档的对比:
类 | 可变文本 | 可变 Span | 数据结构 |
---|
SpannedString | 否 | 否 | 线性数组 | SpannableString | 否 | 是 | 线性数组 | SpannableStringBuilder | 是 | 是 | 区间树 |
这里补充一下,SpannableStringBuilder 实现了 Editable 接口,可以对文本进行 replace 、insert 、append 等操作,所以它是可变文本。下面介绍了如何决定使用哪个类:
- 如果不准备在创建后修改文本或标记,请使用 SpannedString。
- 如果需要将少量 Span 附加到单个文本对象,并且文本本身为只读,请使用 SpannableString。
- 如果需要在创建后修改文本,并且需要将 Span 附加到文本,请使用 SpannableStringBuilder。
- 如果需要将大量 Span 附加到文本对象,那么无论文本本身是否为只读,都请使用 SpannableStringBuilder。这里是由于 SpannableStringBuilder 使用了区间树来实现提高了性能。
flags
SPAN_MARK_MARK、SPAN_MARK_POINT、SPAN_POINT_MARK 和 SPAN_POINT_POINT
当我们在 Span 边界内插入文本,Span 会自动扩展以包含插入的文本。在 Span 边界上(即在 start 或 end 索引处)插入文本时,这四个 flags 参数就是用于确定 Span 是否应扩展以包含插入的文本。下面是使用四个不同 flags 往 测试 文本的 start 和 end 处插入字符串的代码和效果。
String source = "测试";
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder1 = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder2 = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder3 = new SpannableStringBuilder(source);
spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#6aFFFF00")), 0, 2, Spanned.SPAN_MARK_POINT);
spannableStringBuilder1.setSpan(new BackgroundColorSpan(Color.parseColor("#6aFFFF00")), 0, 2, Spanned.SPAN_MARK_MARK);
spannableStringBuilder2.setSpan(new BackgroundColorSpan(Color.parseColor("#6aFFFF00")), 0, 2, Spanned.SPAN_POINT_MARK);
spannableStringBuilder3.setSpan(new BackgroundColorSpan(Color.parseColor("#6aFFFF00")), 0, 2, Spanned.SPAN_POINT_POINT);
spannableStringBuilder.insert(0, "插入数据1");
spannableStringBuilder.insert(spannableStringBuilder.length(), "插入数据2");
spannableStringBuilder1.insert(0, "插入数据1");
spannableStringBuilder1.insert(spannableStringBuilder1.length(), "插入数据2");
spannableStringBuilder2.insert(0, "插入数据1");
spannableStringBuilder2.insert(spannableStringBuilder2.length(), "插入数据2");
spannableStringBuilder3.insert(0, "插入数据1");
spannableStringBuilder3.insert(spannableStringBuilder3.length(), "插入数据2");
mBinding.testRight1.setText(spannableStringBuilder);
mBinding.testRight2.setText(spannableStringBuilder1);
mBinding.testRight3.setText(spannableStringBuilder2);
mBinding.testRight4.setText(spannableStringBuilder3);
看上去是很难记,其实你只要了解 Mark 和 Point 在 Android中表示什么就行了。如下图所示,其实非常简单,Mark 表示在字符左边位置,Point 表示字符右边的位置,光标在 Mark 和 Ponit 中间。与上图的结果对照,是不是豁然开朗😄。
如果你实在记不住,Android 也提供了替代品:SPAN_INCLUSIVE_INCLUSIVE 、SPAN_INCLUSIVE_EXCLUSIVE SPAN_EXCLUSIVE_EXCLUSIVE 、SPAN_EXCLUSIVE_INCLUSIVE。其中 INCLUSIVE 表示包含,EXCLUSIVE 表示不包含,这个是不是好记多了。
其他 flags
其他 flags 的官方描述不怎么清晰,而且网上的信息也比较少。下面的内容是我根据源码和一些介绍得出的结论,不一样准确。
带有 SPAN_INTERMEDIATE 的 Span 被移除时不会调用 SpanWatcher 的 onSpanRemoved 方法。主要应用于文字的选择区域的开始位置,猜测是用来标志选择区域的,如下图所示。
使用 SPAN_PARAGRAPH 的 Span 必须应用于整个文本或者文本中的一个段落。什么是段落呢?在 Android 中,段落结尾处具有一个换行 (‘\n’) 符,如下图所示。
使用 SPAN_PARAGRAPH 的代码示例如下
String source = "哈\n哈哈\n哈哈哈哈哈哈";
SpannableStringBuilder spannableStringBuilder1 = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder2 = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder3 = new SpannableStringBuilder(source);
SpannableStringBuilder spannableStringBuilder4 = new SpannableStringBuilder(source);
spannableStringBuilder1.setSpan(new UnderlineSpan(), 0, spannableStringBuilder1.length(), Spanned.SPAN_PARAGRAPH);
mBinding.testRight1.setText(spannableStringBuilder1);
spannableStringBuilder2.setSpan(new UnderlineSpan(), 0, 2, Spanned.SPAN_PARAGRAPH);
mBinding.testRight2.setText(spannableStringBuilder2);
spannableStringBuilder3.setSpan(new UnderlineSpan(), 2, 5, Spanned.SPAN_PARAGRAPH);
mBinding.testRight3.setText(spannableStringBuilder3);
spannableStringBuilder4.setSpan(new UnderlineSpan(), 5, spannableStringBuilder4.length(), Spanned.SPAN_PARAGRAPH);
mBinding.testRight4.setText(spannableStringBuilder4);
效果如下图: 那这个 flag 是用来做什么的呢?当替换文本时,如果文本中的 Span 满足带有 SPAN_PARAGRAPH ,同时不在要求范围内,就抛弃这个 Span 。 如上图,如果 Span1 带有 SPAN_PARAGRAPH,Span2 没有;则 Span2 会应用在替换的文本上,Span1 不会。
SpannableStringBuilder spannableStringBuilder1 = new SpannableStringBuilder("测试内容");
spannableStringBuilder1.setSpan(new BackgroundColorSpan(Color.parseColor("#6aFFFF00")), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder spannableStringBuilder2 = new SpannableStringBuilder("\n哈哈\n哈哈哈哈哈哈");
spannableStringBuilder2.setSpan(new UnderlineSpan(), 0, 4, Spanned.SPAN_PARAGRAPH);
spannableStringBuilder2.setSpan(new RelativeSizeSpan(1.5f), 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableStringBuilder1.replace(0, 2, spannableStringBuilder2, 0, 3);
mBinding.testRight1.setText(spannableStringBuilder1);
效果如上图,UnderlineSpan 由于设置了 SPAN_PARAGRAPH 被删除了。没有设置的 RelativeSizeSpan 则保留下来了。猜测这个效果应该是避免不同段落的 Span 被错误应用到新文本上。
SPAN_PRIORITY指的是用于更新目的的文本布局的优先级;它只应在特殊情况下设置,因此没有必要由开发者设置。
- SPAN_USER 和 SPAN_USER_SHIFT
SPAN_USER 和 SPAN_USER_SHIFT 是额外的自定义标量数据的存储区域,如果开发人员选择使用它们,它们将与 Span 一起存储。
被 IME(输入法)使用,具体作用未知。
Span
上面的内容总算把 Span相关的文本讲完了,现在才是真正的介绍 Span 了。
在 Android 的官方文档中,把 Span 分为四种,分别是:
影响文本外观的 Span
影响文本外观的 Span:影响文本外观,例如更改文本或背景颜色以及添加下划线或删除线。这些 Span 会实现 UpdateAppearance 并扩展 CharacterStyle。如下图,为文本添加下划线。
代码示例如下:
SpannableString spannableString = new SpannableString("测试文本");
spannableString.setSpan(new UnderlineSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
影响文本指标的 Span
影响文本指标的 Span:影响文本指标,例如行高和文本大小。所有这些 Span 都会扩展 MetricAffectingSpan 类。如下图,将文本大小增加 50%
代码示例如下:
SpannableString string = new SpannableString("测试文本");
string.setSpan(new RelativeSizeSpan(1.5f), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
test.setText(string);
影响单个字符的 Span
影响单个字符的 Span:影响字符级别的文本。例如,您可以更新背景颜色、样式或大小等字符元素。影响单个字符的 Span 会扩展 CharacterStyle 类。如下图,为文本增加背景颜色。 代码示例如下:
SpannableString string = new SpannableString("测试文本");
string.setSpan(new BackgroundColorSpan(Color.YELLOW), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
test.setText(string);
影响段落的 Span
影响段落的 Span:影响段落级别的文本,例如更改整个文本块的对齐方式或边距。影响整个段落的 Span 会实现 ParagraphStyle。
段落 Span 只会影响 \n 左右的样式,不会包括换行符。但是其他类型的 Span 会包含。
如果你尝试将段落 Span 应用于除整个段落以外的其他内容,Android 根本不会应用该 Span。
Android 提供了五种接口,它们都实现了 ParagraphStyle 接口。
- LeadingMarginSpan:处理段落的首行间距和其他行间距
- AlignmentSpan:处理整个段落对其方式;
- LineBackgroundSpan:处理一行的背景;
- LineHeightSpan:处理一行的高度;
- TabStopSpan:将字符串中的"\t"替换成相应的空行;
下面是使用 LeadingMarginSpan 的使用示例。图片和代码来源 What is Leading Margin in Android?
LeadingMarginSpan span = new LeadingMarginSpan.Standard(20, 100);
LeadingMarginSpan span = new LeadingMarginSpan.Standard(100, 0);
自定义 Span
文本相关的自定义 Span 非常简单,它是通过修改 TextPain 的属性来实现自定义效果的。下面是实现可用于修改文本大小和颜色的自定义 Span的官方示例。
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 与文本相关的 Span 不同,无法概括。这里以文本环绕为例,代码如下。原理很简单,就是通过 图片的高 / 行高 获取需要设置的行数,并对指定行返回 图片宽度 + padding 的值就行了。
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(source);
spannableStringBuilder.setSpan(new LeadingMarginSpan.LeadingMarginSpan2() {
@Override
public int getLeadingMarginLineCount() {
int count = mImageView.getHeight() / mTextView.getLineHeight();
return count;
}
@Override
public int getLeadingMargin(boolean first) {
if (first) {
return mImageView.getWidth() + 20;
} else {
return 0;
}
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top,
int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
}
}, 0, spannableStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTextView.setText(spannableStringBuilder);
效果如图:
总结
这篇文章着重介绍了 Spannable 和 Spanned 接口的方法和 flags 参数的影响;同时补充了 Span 的分类、一些常用 Span 的使用以及如何自定义 Span 等。最后求求点个免费的赞
参考
-Span guide -Explain the definitions of 0these flags -What is Leading Margin in Android?
|