我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

Span 是什么

Android Span 原理解析

看上图中的 Span 是什么?假如你回答是测验或许测验及下面的下划线那就错了。咱们看一下 Google 的界说

Span 是功能强大的符号目标,可用于在字符或阶段等级为文本设置款式。经过将 Span 附加到文本目标,能够以各种办法更改文本,包含添加色彩、使文本可点击、缩放文本巨细以及以自界说办法制作文本。Span 还能够更改 TextPaint 特点、在 Canvas 上制作,以及更改文本布局。

简略的说,Span 是用来处理指定范围内的文本款式的东西。如上图,Span 就为[0, 1]范围内的文本设置了下划线的款式。这儿是为了告知你 Span 不是文本,它是文本处理的东西。真实的文本是分别完成SpannableSpanned接口的三个类,分别是SpannedStringSpannableStringSpannableStringBuilder。这三个完成类稍后再讲,咱们先看一下SpannableSpanned接口。

Spannable 和 Spanned 接口

SpannableSpanned的差异很简略,Spanned只能获取Span,但Spannable能够设置和修正SpanSpannable承继Spanned,并添加了setSpanremoveSpan办法来设置和修正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吗?答案是SpannedStringSpannableString能够;而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);
//修正 Span 的方位
spannableString.setSpan(underlineSpan, 2, 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(spannableString);
//由于只判别目标是否相同,导致了添加了2个完全相同的 Span
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的部分,如下图。

Android Span 原理解析

但是,getSpan办法不光会获取start < pos < end的部分,还会获取部分方位在范围内的 Span 。如下图所示:

Android 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 了。

Android Span 原理解析

public int nextSpanTransition(int start, int limit, Class kind)

回来一个类型的 Span 开端或完毕的第一个大于start的偏移,假如没有大于start但小于limit的开端或完毕,则回来limit。看文字有点难理解,直接上图。

Android Span 原理解析

除了图上的两种状况外,nextSpanTransition办法只会回来传入的 limit 的值。你没看错,即使 SpanStart == start 时也是回来 limit 的值。

Android Span 原理解析
其他 Spanned 界说的办法的介绍如下:

  • public int getSpanStart(Object what): 获取指定 Span 的 start 的值。没找到回来 -1
  • public int getSpanEnd(Object what): 获取指定 Span 的 end 的值。没找到回来 -1
  • public int getSpanFlags(Object tag): 获取 Span 的 flag。没有则回来 0

SpannedString 、SpannableString 和 SpannableStringBuilder

对于这三个类,直接看下面官方文档的比照:

可变文本 可变 Span 数据结构
SpannedString 线性数组
SpannableString 线性数组
SpannableStringBuilder 区间树

这儿弥补一下,SpannableStringBuilder完成了Editable接口,能够对文本进行replaceinsertappend等操作,所以它是可变文本。下面介绍了怎么决议运用哪个类:

  • 假如不准备在创立后修正文本或符号,请运用 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 往测验文本的startend处刺进字符串的代码和作用。

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);

Android Span 原理解析

看上去是很难记,其实你只需了解 Mark 和 Point 在 Android中表明什么就行了。如下图所示,其实十分简略,Mark 表明在字符左边方位,Point 表明字符右边的方位,光标在 Mark 和 Ponit 中间。与上图的成果对照,是不是豁然开朗。

Android Span 原理解析

假如你真实记不住,Android 也提供了替代品:SPAN_INCLUSIVE_INCLUSIVE 、SPAN_INCLUSIVE_EXCLUSIVE SPAN_EXCLUSIVE_EXCLUSIVE 、SPAN_EXCLUSIVE_INCLUSIVE。其中 INCLUSIVE 表明包含,EXCLUSIVE 表明不包含,这个是不是好记多了。

其他 flags

其他 flags 的官方描述不怎么清晰,并且网上的信息也比较少。下面的内容是我依据源码和一些介绍得出的定论,不一样准确。

  • SPAN_INTERMEDIATE

带有 SPAN_INTERMEDIATE 的 Span 被移除时不会调用 SpanWatcher 的onSpanRemoved办法。首要运用于文字的挑选区域的开端方位,猜想是用来标志挑选区域的,如下图所示。

Android Span 原理解析

  • SPAN_PARAGRAPH

运用 SPAN_PARAGRAPH 的 Span 有必要运用于整个文本或许文本中的一个阶段。什么是阶段呢?在 Android 中,阶段结尾处具有一个换行 (‘\n’) 符,如下图所示。

Android Span 原理解析

运用 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);

作用如下图:

Android Span 原理解析

那这个 flag 是用来做什么的呢?当替换文本时,假如文本中的 Span 满意带有 SPAN_PARAGRAPH ,同时不在要求范围内,就扔掉这个 Span 。

Android 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);

Android Span 原理解析

作用如上图,UnderlineSpan 由于设置了 SPAN_PARAGRAPH 被删去了。没有设置的 RelativeSizeSpan 则保存下来了。猜想这个作用应该是避免不同阶段的 Span 被错误运用到新文本上。

  • SPAN_PRIORITY

SPAN_PRIORITY指的是用于更新意图的文本布局的优先级;它只应在特别状况下设置,因而没有必要由开发者设置。

  • SPAN_USER 和 SPAN_USER_SHIFT

SPAN_USER 和 SPAN_USER_SHIFT 是额外的自界说标量数据的存储区域,假如开发人员挑选运用它们,它们将与 Span 一起存储。

  • SPAN_COMPOSING

被 IME(输入法)运用,详细作用不知道。

Span

上面的内容总算把 Span相关的文本讲完了,现在才是真实的介绍 Span 了。

在 Android 的官方文档中,把 Span 分为四种,分别是:

影响文本外观的 Span

影响文本外观的 Span:影响文本外观,例如更改文本或布景色彩以及添加下划线或删去线。这些 Span 会完成 UpdateAppearance 并扩展 CharacterStyle。如下图,为文本添加下划线。

Android Span 原理解析

代码示例如下:

SpannableString spannableString = new SpannableString("测验文本");
spannableString.setSpan(new UnderlineSpan(), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

影响文本目标的 Span

影响文本目标的 Span:影响文本目标,例如行高和文本巨细。一切这些 Span 都会扩展 MetricAffectingSpan 类。如下图,将文本巨细添加 50%

Android Span 原理解析

代码示例如下:

SpannableString string = new SpannableString("测验文本");
string.setSpan(new RelativeSizeSpan(1.5f), 0, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
test.setText(string);

影响单个字符的 Span

影响单个字符的 Span:影响字符等级的文本。例如,您能够更新布景色彩、款式或巨细等字符元素。影响单个字符的 Span 会扩展 CharacterStyle 类。如下图,为文本添加布景色彩。

Android Span 原理解析

代码示例如下:

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); // left example
LeadingMarginSpan span = new LeadingMarginSpan.Standard(100, 0); // right exmaple

Android Span 原理解析

自界说 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() {
        //获取需求设置margin的行数
        int count = mImageView.getHeight() / mTextView.getLineHeight();
        return count;
    }
    @Override
    public int getLeadingMargin(boolean first) {
        if (first) {
            //回来 margin 的值
            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);

作用如图:

Android Span 原理解析

总结

这篇文章着重介绍了SpannableSpanned接口的办法和 flags 参数的影响;同时弥补了 Span 的分类、一些常用 Span 的运用以及怎么自界说 Span 等。

最后,求求点个免费的赞吧。

参阅

  • Span guide
  • Explain the definitions of 0these flags
  • What is Leading Margin in Android?