Android中Span研究

来源:互联网 发布:java虚拟机书籍推荐 编辑:程序博客网 时间:2024/05/05 02:25

最近开始学习Android,前天在Github上找到了一个Android下记事本的源代码,看着挺简单就想学习一下,可是还是被一些基础的问题难倒。做了一整天的研究才有些理解。
Github链接:https://github.com/mthli/Knife
这个文本编辑器要实现对文字的加粗、斜体、下划线、删除线等功能。在实现这些功能时,用到了Span的概念。Span是Android中文本操作的一个重要概念,与其相关的类包括Spannable、Spanned等。Span的具体内容以下两篇文章有详细介绍:
http://blog.csdn.net/lixin84915/article/details/8110667
http://flavienlaurent.com/blog/2014/01/31/spans/

一、遇到的问题
在这个app的源代码中,设置字体样式的代码是这样的:

protected void styleValid(int style, int start, int end) {        switch (style) {            case Typeface.NORMAL:            case Typeface.BOLD:            case Typeface.ITALIC:            case Typeface.BOLD_ITALIC:                break;            default:                return;        }        if (start >= end) {            return;        }        //getEditableText返回值为Editable,继承自Spannable类        //setSpan设置字体        getEditableText().setSpan(new StyleSpan(style), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);    }

这里用到了setSpan函数:
public abstract void setSpan (Object what, int start, int end, int flags)
函数的第一个参数是要设置的样式,这段代码中使用了StyleSpan,该类可以设置字体的粗体和斜体,还可以使用UnderlineSpan(设置下划线)等类。第二个和第三个参数限定了要设置的文本的范围。第四个参数在这里不重要。

取消字体的代码是这样的:

protected void styleInvalid(int style, int start, int end) {        switch (style) {            case Typeface.NORMAL:            case Typeface.BOLD:            case Typeface.ITALIC:            case Typeface.BOLD_ITALIC:                break;            default:                return;        }        if (start >= end) {            return;        }        StyleSpan[] spans = getEditableText().getSpans(start, end, StyleSpan.class);        List<KnifePart> list = new ArrayList<>();        for (StyleSpan span : spans) {            if (span.getStyle() == style) {                //getSpanStart和getSpanEnd返回span的初始和结束位置                list.add(new KnifePart(getEditableText().getSpanStart(span), getEditableText().getSpanEnd(span)));                //移除当前span格式                getEditableText().removeSpan(span);            }        }        //疑问代码段        for (KnifePart part : list) {            if (part.isValid()) {                if (part.getStart() < start) {                    styleValid(style, part.getStart(), start);                }                if (part.getEnd() > end) {                    styleValid(style, end, part.getEnd());                }            }        }    }

首先使用getSpans函数获取目标文本的span:
public abstract T[] getSpans (int start, int end, Class type)
该函数的第一和第二个参数限定了获取的文本的范围。
但是通过调试,发现了两个疑问。
1、在removeSpan(span)这个函数中只指明了span,并未指明文本范围,函数是如何知道移除哪段文本的span呢?
2、那段有疑问的代码段一开始并没有看明白,感觉好像没什么作用,后来被我注释掉再调试,发现疑问:
比如这段文字:1234567890
这串数字是斜体,当我使用getSpans(2,6,StyleSpan)获取“3456”这段文本的span,再调用removeSpan(span)移除时,程序将整个“1234567890”的斜体格式全部移除了!
字符全部是斜体
只改变3456的字体,却改变了整个字符串的字体

当我将注释掉的代码取消注释后,问题就解决了。
问题解决了

二、疑问分析
经过调试分析,在下面这段代码中找到产生问题的原因:
list.add(new KnifePart(getEditableText().getSpanStart(span), getEditableText().getSpanEnd(span)));
这段代码中的getSpanStart(span)和getSpanEnd(span)这两个函数获取getSpans返回的span文本的起始和结束位置。虽然我在getSpans中指定了span的文本范围是2~6,但是调试发现,这两个函数获取的结果并不是2~6而是0~10:
范围是0~10
也就是说,我们getSpans函数获取到的span的范围是整个“1234567890”字符串,这样在removeSpan的时候当然就将整个字符串的格式改变了。

三、问题成因
为什么会这样呢?我尝试使用TextUtils#dumpSpans函数来将整个文本的span全部列出看看。dumpSpans函数原型如下:

public static void dumpSpans (CharSequence cs, Printer printer, String prefix)

我在代码中加入如下两句:

Printer printer = new LogPrinter(Log.DEBUG, "TAG");TextUtils.dumpSpans(getEditableText(),printer,"spans: ");

之后尝试取消“3456”的斜体格式
将所有的span输出到logcat中查看,结果如下:
这里写图片描述
其中红色框内显示了我们要修改斜体的文本范围即2~6。
其中最后一条是我们想修改斜体文本的span,可以看出,它的文本是1234567890,范围是0~10,而且它有一个类似ID的值 b17fd86。

这时候我们的问题已经有了大致的答案,是否getSpans函数得到的span是与所选文本范围格式相同且相连的文本范围呢?为了确定我们的想法,我换了方法进行调试。
首先我将“123”这三个字符设置成斜体格式,再将“890”这三个字符设置成斜体,然后调试看dumpSpans的输出结果,发现这时候产生了两个StyleSpan格式的span:
这里写图片描述
两个span的文本和我们上面设置的一样,分别是“123”和“890”。
接下来我将“123”和“890”中间的“4567”也设置为斜体,这时候“1234567890”这个字符串已经全部变为斜体。这时我尝试取消“345678”的文本格式,调试发现:
1、刚才“123”和“890”的span并未消失,而是多出来了“4567”的span(这里“123”和“890”的span的ID值应该是不变的,“890”ID的改变是因为调试时不小心修改错了,之后懒得再重新调试截图了。。。)
这里写图片描述
2、getSpans函数返回的span数组中包含三个span:
这里写图片描述

四、结论
这时候我的答案就浮出水面了:当我们每次setSpan为某范围的文本设置字体格式的时候,编译器会自动将这个span设置一个ID,不论将来相邻文本的格式是否与其相同,都不会改变这个span,它始终是独一无二的。当使用getSpans获取某段文本A的span时,会返回一个目标span格式的span数组,文本A包含几个span,数组里就会有几个span。比如在上面的测试中“1234567890”这段文本包含“123”“4567”和“890”这三段来自不同span的文本。当我们调用getSpans函数来获取“345678”这段文本格式时,就会返回三个span,这三个span分别是“123”“4567”和“890”的span,因此在循环中调用removeSpan移除文本格式时,就会修改整个“1234567890”的文本格式,必须在之后对“345678”范围外的字符加以还原,才能得到想要的结果。

0 0
原创粉丝点击