真·富文本编辑器的演进之路-富文本Span的边界探究

Span是Android文本系统中一个非常重要的功能,对于它的一般使用,其实比较简单,但在处理一些复杂业务时,Span的边界问题处理就显得非常重要了,不然很容易因为边界情况没有处理好,导致一系列很麻烦的bug。

setSpan

with(binding) {
    val text = "我真的是被Span搞裂开了"
    SpannableString(text).also {
        it.setSpan(StyleSpan(Typeface.BOLD_ITALIC), 0, text.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
        this.textview.text = it
    }
}
复制代码

注意这里的range,start…end,end是text.length,正好将所有文字Span化,如果start…end超过0…text.length的区间,那么就会产生IndexOutOfBoundsException,由此可知,setSpan中的range,是一个左闭右开区间。

[ start … end ) —— [ 0 … length )

getSpans

with(binding.textview) {
    val spannableString = SpannableString(text)
    val spans = spannableString.getSpans(0, length(), StyleSpan::class.java)
    spans.forEach { span ->
        val start = spannableString.getSpanStart(span)
        val end = spannableString.getSpanEnd(span)
        Log.d("xys", "getSpans: Start: $start , End: $end")
    }
}
复制代码

与setSpan类似,我们通过getSpans来找到range里面的所有指定类型span,那么这里的start…end呢,我们先试下0…length,0…length - 1,0…length + 1,-1…length,-1…length + 1,length - 1…length + 1,-1…1这几种情况。

不出意外,这几种都可以获取出正确的Span。

再来看看length…length + 1,-1…0这两种情况。

出意外了,这时候就获取不到了。

总结一下,来张图就看清楚了。

image-20211202110043219

红色的范围是不可获取,灰色的范围是可以获取,由此可见,getSpans比setSpan的range要复杂多了。

总结一下,对于一个Span,范围是0…Length-1,那么getSpans的range,start…end能获取到Span的条件是,start…end完全落在0…Length-1的左开右闭区间里。

最常用的方式,实际上就是:

getSpans(length() - 1, length(), StyleSpan::class.java)
复制代码

Span原理分析

我们借助SpannableStringInternal来分析Span具体是如何作用到Text上的。

要想把Span附加到Text上,那么肯定是对Text做了标记,在渲染时,根据标记来做特殊的渲染。

123davdzz

这是Spannable相关的类继承关系。

  • 对于SpannedString、SpannableString来说,它们是继承的SpannableStringInternal。
  • Span是否是可变,是通过Spanned(Span不能增删)和Spannable(Span可以增删)接口来区分的。

所以核心逻辑都在SpannableStringInternal中,在它的源码中,有几个重要的成员变量:

  • mSpans:用来保存具体的Span对象
  • mSpanData:用来保存每个Span的数据,start、end、flag

在mSpanData中,每个Span需要三个元素来控制,所以,mSpanData的长度是3的倍数,每3个元素代表一个Span,从下面这张图就能看的很清楚了。

img

下面继续来看SpannableStringInternal的构造函数。

SpannableStringInternal的构造函数,就是为了初始化上面的成员变量,它有两个来源,一个本身就是SpannableStringInternal,那么直接继承它内部的这些变量即可,另一个是其它类型,就需要重新创建。

private static final int START = 0;
private static final int END = 1;
private static final int FLAGS = 2;
private static final int COLUMNS = 3;

int start = mSpanData[i * COLUMNS + START];
int end = mSpanData[i * COLUMNS + END];
int flag = mSpanData[i * COLUMNS + FLAGS];
复制代码

在了解了Text如何保存Span及其数据后,我们来看下getSpans为什么会有上面那么奇葩的设计。

原因就在getSpans代码中的check逻辑。

    public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
        int count = 0;

        int spanCount = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;
        Object[] ret = null;
        Object ret1 = null;

        for (int i = 0; i < spanCount; i++) {
            int spanStart = data[i * COLUMNS + START];
            int spanEnd = data[i * COLUMNS + END];

            if (spanStart > queryEnd) {
                continue;
            }
            if (spanEnd < queryStart) {
                continue;
            }

            if (spanStart != spanEnd && queryStart != queryEnd) {
                if (spanStart == queryEnd) {
                    continue;
                }
                if (spanEnd == queryStart) {
                    continue;
                }
            }
复制代码

就是这里的一堆判断逻辑,导致了前面略显奇葩的结果。

看到这里,应该就能明白了,我们传入的range(queryStart…queryEnd)和(spanStart…spanEnd)之间究竟是怎么比较的。

要通过check,必须依次保证下面的条件(以-1…0为例):

  • End >= SpanStart 0 >= 0 true
  • Start <= SpanEnd -1 <= 13 true
  • SpanStart != SpanEnd && Start != End true
  • End != SpanStart 0 != 0 false
  • SpanEnd != Start 13 != -1 true

由此可见,这些条件check的实际上是query的End和SpanStart,以及query的Start和SpanEnd之间的关系。

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问

猜你喜欢

转载自juejin.im/post/7039637143173955615