https://blog.csdn.net/xueshanhaizi/article/details/51398320
引言
在应用程序开发过程经常需要对文本进行处理,比如说对一段描述文字的其中一段加入点击事件,或者对其设置不一样的前景色,有什么方法可以实现要求的功能呐?
需求样例
比如我们需要实现如下图所示的功能,将文本:#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview) 处理成第二种或者第三种的形式。
实现方案
根据上图,我们可以采用如下的方法来实现上诉要求的效果。
方案1
比如显示效果二你可以能会说,我们可以采用三个TextView来实现,第一个TextView设置不一样的颜色,第二个正常显示内容,第三个处理点击事件。该方式对图二可能是能够实现的,但是如果第二行里面就有部分内容需要进行点击处理,就比较难以实现了。
对于图三的效果上述的方式就很难实现了。必须要对TextView的内容进行处理了!!
方案2
如果文案的处理只是简单的对齐,颜色,大小的变换,我们还可以采用自定义view来实现,在前面的文章中我们就采用了自定义view来显示了一个文字的排版效果,具体实现可以查看Android文本排版实现;
方案3
除了上面的方案,我们还可以采用另外一个种方式来实现,采用html来显示,可以将要显示的内容转换成html的格式,用TextView来进行加载。说了这么多,我们来看看代码吧!
private void setText() {
String originText = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview)";
String effect1 = "<font color='#FF0000'>#重磅消息#</font> <br> 近日谷歌放出Android " +
"N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";
String effect2 = "<font color='#303F9F'>#重磅消息#</font> 近日谷歌放出Android " +
"N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";
StringBuilder sb = new StringBuilder(originText);
sb.append("<br><br><br><br>");
sb.append(effect1);
sb.append("<br><br><br><br>");
sb.append(effect2);
textView.setText(Html.fromHtml(sb.toString()));
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
写到这,突然发现要跑题,仅仅是Html的实现就可以分析出很多的知识点,不过这里还是先契合主题,先这里挖一个坑,后续对html进行分析,Android Html
解析
方案4
终于回到我们的主题了,这里我们采用SpannableString来实现上述的效果。代码如下:
private void setSpan() {
String originText = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview)";
SpannableStringBuilder sb = new SpannableStringBuilder(originText);
sb.append("\r\n").append("\r\n").append("\r\n");
getEffect1Span(sb);
sb.append("\r\n").append("\r\n").append("\r\n");
getEffect2Span(sb);
textView.setText(sb);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
private void getEffect1Span(SpannableStringBuilder sb) {
String source1 = "#重磅消息#";
SpannableString span = new SpannableString(source1);
span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.append(span);
sb.append("\n");
String source2 = "近日谷歌放出Android N的第二个开发者预览版";
sb.append(source2);
final String source3 = "(Developer Preview)";
SpannableString clickSpan = new SpannableString(source3);
clickSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
ToastUtil.showLong(source3);
}
}, 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.append(clickSpan);
}
private void getEffect2Span(SpannableStringBuilder sb) {
String source1 = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版";
SpannableString span = new SpannableString(source1);
span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimaryDark)), 0, 6, Spanned
.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.append(span);
final String source2 = "(Developer Preview)";
SpannableString clickSpan = new SpannableString(source2);
clickSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
ToastUtil.showLong(source2);
}
}, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.append(clickSpan);
}
上述代码采用了硬编码方式实现,正常实现,需要根据需求进行设置。记得要添加textView.setMovementMethod(LinkMovementMethod.getInstance());来接受点击事件。
SpnnableString详解
SpannableString继承了SpannableStringInternal,同时实现了CharSequence, GetChars, Spannable三个接口,正常处理文本的函数为setSpan函数:
public void setSpan(Object what, int start, int end, int flags) {
super.setSpan(what, start, end, flags);
}
该函数有四个参数,第一个为一个span类型,第二个参数为开始位置,第三个位置为span的结束位置,最后一个为flag参数。
what可以设置如下类型:
1, AbsoluteSizeSpan 设置文字字体的绝对大小, 有两个参数,第一个是字体大小,第二个是单位是否是dip
public AbsoluteSizeSpan(int size, boolean dip) {
mSize = size;
mDip = dip;
}
2,AlignmentSpan 主要设置文本的对齐方式,有三种方式正常,居中,相反的方式对齐,默认实现为Standard
public Standard(Layout.Alignment align) {
mAlignment = align;
}
3,BackgroundColorSpan 设置文字的背景色
private void setfCS(){
String source1 = "#重磅消息#";
SpannableString span = new SpannableString(source1);
span.setSpan(new BackgroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(span);
}
4,BulletSpan 给文本的开始处加上项目符号。比如前面加一个 .
private void setBSpan() {
final String source3 = "近日谷歌放出Android N的第二个开发者预览版";
SpannableString bSpan = new SpannableString(source3);
bSpan.setSpan(new BulletSpan(), 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(bSpan);
}
5, ClickableSpan 设置文本的点击事件,要实现onClick函数,可以复写updateDrawState,设置下划线,或者取消下划线,还可以设置下划线颜色
private void setCS(){
final String source2 = "(Developer Preview)";
SpannableString clickSpan = new SpannableString(source2);
clickSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
ToastUtil.showLong(source2);
}
}, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(clickSpan);
}
6,DrawableMarginSpan 可以设置一个图标,并且可以设置与文字的宽度
private void setDMSpan() {
final String source3 = "(Developer Preview)";
SpannableString dmSpan = new SpannableString(source3);
dmSpan.setSpan(new DrawableMarginSpan(getResources().getDrawable(R.mipmap.ic_launcher), 30), 0, source3
.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(dmSpan);
}
7,DynamicDrawableSpan 设置某段文字被图标替换,需要返回一个drawable
8,EasyEditSpan 当文本改变或者删除时调用, 例如入下长按可以很容易删除一行
private void setEdit() {
editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setSingleLine(false);
editText.setText("近日\n谷歌放出Android N的\n第二个开发者预览版");
editText.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
final Layout layout = editText.getLayout();
final int line = layout.getLineForOffset(editText.getSelectionStart());
final int start = layout.getLineStart(line);
final int end = layout.getLineEnd(line);
editText.getEditableText().setSpan(new EasyEditSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return true;
}
});
}
9,ForegroundColorSpan 设置文字前景色
private void setfCS(){
String source1 = "#重磅消息#";
SpannableString span = new SpannableString(source1);
span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(span);
}
写到这里我停下来了。天啦噜,30多个span,可以去系统代码package android.text.style包下查看,这么多,整个人都不好了。
因此先就针对上面的做了部分样例,之后会专门实现一下每个span的效果。仔细理解一个就行,其他的都是类似的,我们继续看看后面的参数。
第二参数start和第三个参数end,表示当时设置的span作用效果的范围,start表示开始位置,end表示结束位置,第四个参数是一个flag标签。这里主要设置以下的值:
/**
* Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand
* to include text inserted at their starting point but not at their
* ending point. When 0-length, they behave like marks.
*/
public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;
/**
* Spans of type SPAN_INCLUSIVE_INCLUSIVE expand
* to include text inserted at either their starting or ending point.
*/
public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;
/**
* Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand
* to include text inserted at either their starting or ending point.
* They can never have a length of 0 and are automatically removed
* from the buffer if all the text they cover is removed.
*/
public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;
/**
* Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand
* to include text inserted at their ending point but not at their
* starting point. When 0-length, they behave like points.
*/
public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;
常用的就是上述的四个值,这里我们来分别解释以下:
1. SPAN_INCLUSIVE_EXCLUSIVE表示左闭右开区间 “[ )”
2. SPAN_INCLUSIVE_INCLUSIVE表示左右都是闭区间 ‘( )’
3. SPAN_EXCLUSIVE_EXCLUSIVE表示左右都是闭区间 ‘[ ]’
4. SPAN_EXCLUSIVE_INCLUSIVE表示左右都是闭区间 ‘( ]’
我们继续来看代码,SpannableString的setSpan又继续调用了SpannableStringInternal的setSpan函数。
/* package */ void setSpan(Object what, int start, int end, int flags) {
int nstart = start;
int nend = end;
checkRange("setSpan", start, end);
if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
if (start != 0 && start != length()) {
char c = charAt(start - 1);
if (c != '\n')
throw new RuntimeException(
"PARAGRAPH span must start at paragraph boundary" +
" (" + start + " follows " + c + ")");
}
if (end != 0 && end != length()) {
char c = charAt(end - 1);
if (c != '\n')
throw new RuntimeException(
"PARAGRAPH span must end at paragraph boundary" +
" (" + end + " follows " + c + ")");
}
}
int count = mSpanCount;
Object[] spans = mSpans;
int[] data = mSpanData;
for (int i = 0; i < count; i++) {
if (spans[i] == what) {
int ostart = data[i * COLUMNS + START];
int oend = data[i * COLUMNS + END];
data[i * COLUMNS + START] = start;
data[i * COLUMNS + END] = end;
data[i * COLUMNS + FLAGS] = flags;
sendSpanChanged(what, ostart, oend, nstart, nend);
return;
}
}
if (mSpanCount + 1 >= mSpans.length) {
Object[] newtags = ArrayUtils.newUnpaddedObjectArray(
GrowingArrayUtils.growSize(mSpanCount));
int[] newdata = new int[newtags.length * 3];
System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);
mSpans = newtags;
mSpanData = newdata;
}
mSpans[mSpanCount] = what;
mSpanData[mSpanCount * COLUMNS + START] = start;
mSpanData[mSpanCount * COLUMNS + END] = end;
mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
mSpanCount++;
if (this instanceof Spannable)
sendSpanAdded(what, nstart, nend);
}
/* package */ void removeSpan(Object what) {
int count = mSpanCount;
Object[] spans = mSpans;
int[] data = mSpanData;
for (int i = count - 1; i >= 0; i--) {
if (spans[i] == what) {
int ostart = data[i * COLUMNS + START];
int oend = data[i * COLUMNS + END];
int c = count - (i + 1);
System.arraycopy(spans, i + 1, spans, i, c);
System.arraycopy(data, (i + 1) * COLUMNS,
data, i * COLUMNS, c * COLUMNS);
mSpanCount--;
sendSpanRemoved(what, ostart, oend);
return;
}
}
}
首先调用了checkRange,判断了位置的合法性,如果start小于end,或者位置下标越界都会抛出IndexOutOfBoundsException异常。
之后判断了(flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH是否相等,这里如果设置的是上述四个值,这里是不等的,所以不会进入该判断。
设置了count,第一次count为0,设置了spans数组与data,第一次设置的值是在构造函数中初始化的值。
因为count为0,因此for循环也不会进入
之后判断了mSpanCount + 1 >= mSpans.length,这里前面为1,后面为0,因此会进入if判断,首先申请了一个3个长度的newtags数组,一个9个长度的int数组, 之后进行了两次数据拷贝,将已有的span拷贝到新申请的数组中,将其他参数拷贝到新的int数组中。
之后将改成设置的span设置到mSpans数组中,将其他的参数设置到mSpanData,三个参数是连续设置的。
最后调用了sendSpanAdded,代码如下:
private void sendSpanAdded(Object what, int start, int end) {
SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
int n = recip.length;
for (int i = 0; i < n; i++) {
recip[i].onSpanAdded((Spannable) this, what, start, end);
}
}
这个调用了getSpans,返回了一个SpanWatcher数组,SpanWatcher是一个接口,MultiTapKeyListener, TextKeyListener实现了该类,因此当调用了TextKeyListener或者MultiTapKeyListener会对相应的span进行处理。
总结
这里只是大致的解析了SpannableString,他还需要结合TextView进行分析,看看在界面绘制的时候是怎样解析显示的。后续有时间会陆续进行解析的。
最后附一个链接,在我解析span的时候,解析了几个感觉太多,就搜索一下是否已经有人解析过,因此这个这里加上跳转链接,如果有版权或者不让导航,请告知,我好删除。传送门