您的位置:首页 > 移动开发 > Android开发

Android中Span研究

2015-12-18 22:37 411 查看
最近开始学习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”的斜体格式全部移除了!





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



二、疑问分析

经过调试分析,在下面这段代码中找到产生问题的原因:

list.add(new KnifePart(getEditableText().getSpanStart(span), getEditableText().getSpanEnd(span)));


这段代码中的getSpanStart(span)和getSpanEnd(span)这两个函数获取getSpans返回的span文本的起始和结束位置。虽然我在getSpans中指定了span的文本范围是2~6,但是调试发现,这两个函数获取的结果并不是2~6而是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: ");



4000
后尝试取消“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”范围外的字符加以还原,才能得到想要的结果。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android Span Spannable