对于富文本编辑器来说,除了插入图片,最重要的功能之一应该就是提供不同的文字样式了。其中主要包括:加粗、斜体、切换文字颜色等。与图片不同,一般来说我们对于文字样式的产品需求包括两个方面:
- 设置文字样式;
- 获取某段文字的当前样式;
先说设置,可以参考这篇文章:
http://hunankeda110.iteye.com/blog/1420470
//设置字体前景色
msp.setSpan(new ForegroundColorSpan(Color.MAGENTA), 12, 15, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置前景色为洋红色
//设置字体背景色
msp.setSpan(new BackgroundColorSpan(Color.CYAN), 15, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //设置背景色为青色
//设置字体样式正常,粗体,斜体,粗斜体
msp.setSpan(new StyleSpan(android.graphics.Typeface.NORMAL), 18, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //正常
msp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 20, 22, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); //粗体
msp.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 22, 24, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); //斜体
msp.setSpan(new StyleSpan(android.graphics.Typeface.BOLD_ITALIC), 24, 27, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); //粗斜体
这个其实在之前本系列的文章中就介绍过,是spannable string的基本用法。但是为了写出扩展性好、更工程化的方法,满足修改、查找、添加等不同的需求,只有这个还是不行的。在网上找了很多源码和资料后,向大家推荐这个项目的写法:
https://github.com/1gravity/Android-RTEditor
这个项目实现的富文本编辑器非常强大,几乎可以完成所有富文本的功能,笔者也从中获益良多。这个项目中采用了类似工厂方法的模式,提供了一个Effect抽象基类,所有的文字样式都继承自该基类,并负责构造对应的Span,在基类中实现了apply方法,应用不同的文字样式。
如下,是一个BoldEffect的例子:
public class BoldEffect extends Effect<Boolean> {
@Override
protected Class<? extends Span> getSpanClazz() {
return BoldSpan.class;
}
@Override
protected Span<Boolean> newSpan(Boolean value) {
return value ? new BoldSpan() : null;
}
}
BoldSpan的实现如下:
public class BoldSpan extends StyleSpan implements Span<Boolean> {
public BoldSpan() {
super(Typeface.BOLD);
}
@Override
public Boolean getValue() {
return Boolean.TRUE;
}
}
下面我们看一下Effect的源码,理解上面子类中getSpanClazz和newSpan的应用场景。首先是判断当前样式在选定的文本中是否存在,代码如下:
final public boolean existsInSelection(RichEditText editor, int spanType) {
Selection expandedSelection = getExpandedSelection(editor, spanType);
if (expandedSelection != null) {
Span<V>[] spans = getSpans(editor.getText(), expandedSelection);
return spans.length > 0;
}
return false;
}
final public Span<V>[] getSpans(Spannable str, Selection selection) {
Class<? extends Span> spanClazz = getSpanClazz();
Span<V>[] result = str.getSpans(selection.start(), selection.end(), spanClazz);
return result != null ? result : (Span<V>[]) Array.newInstance(spanClazz);
}
这里使用的getSpans方法是在Spanned接口中定义的,并在SpannableString中提供了实现。接口定义如下:
/**
* Return an array of the markup objects attached to the specified
* slice of this CharSequence and whose type is the specified type
* or a subclass of it. Specify Object.class for the type if you
* want all the objects regardless of type.
*/
public <T> T[] getSpans(int start, int end, Class<T> type);
上面调用过程中,使用了子类中重载的getSpanClazz方法。
下面再来看看apply方法的实现:
public void apply(RichEditText editor, int start, int end, V value) {
Selection selection = new Selection(start, end);
Spannable str = editor.getText();
// expand the selection to "catch" identical leading and trailing styles
Selection expandedSelection = selection.expand(1, 1);
for (Span<V> span : getSpans(str, expandedSelection)) {
boolean equalSpan = span.getValue() == value;
int spanStart = str.getSpanStart(span);
if (spanStart < selection.start()) {
if (equalSpan) {
selection.offset(selection.start() - spanStart, 0);
}
else {
str.setSpan(newSpan(span.getValue()), spanStart, selection.start(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
int spanEnd = str.getSpanEnd(span);
if (spanEnd > selection.end()) {
if (equalSpan) {
selection.offset(0, spanEnd - selection.end());
}
else {
str.setSpan(newSpan(span.getValue()), selection.end(), spanEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
}
str.removeSpan(span);
}
if (value != null) {
Span<V> newSpan = newSpan(value);
if (newSpan != null) {
int flags = selection.isEmpty() ? Spanned.SPAN_INCLUSIVE_INCLUSIVE : Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
str.setSpan(newSpan, selection.start(), selection.end(), flags);
}
}
}
这段最重要的代码就是从if (value != null)开始,设置样式span,其中用到了子类的newSpan方法。我们注意到,如果selection为空,则设置成Spanned.SPAN_INCLUSIVE_INCLUSIVE,而不为空是Spanned.SPAN_EXCLUSIVE_INCLUSIVE。一般来说,文字样式应该设置成SPAN_EXCLUSIVE_INCLUSIVE,这样后续添加的文字就可以采用相同样式。但在selection为空时,前后都需要采用同样的样式,这样再插入新的文字就可以达到该效果。
而前面for循环的部分,其实是在检测之前的这段文字中是否已经设置了样式span。举例说明,如果一段文字长度为20,其中1-8个字符已经被设置了A样式,12-20个字符已经被设置了B样式,现在要给5-15个字符设置C样式,那么我们进行下面三个步骤:
- 将A样式span调整为1-5个字符(原来是1-8);
- 将B样式span调整为16-20个字符(原来是12-20);
- 再将C样式span设置在5-15个字符上;
对照上述逻辑,再看for循环中的代码,就可以很好地理解了。大家也可以再对照代码来看,可以查看上面所贴链接。当然也可以在我的工程代码,其中只有BoldEffect,但是也有类似Effect的实现,但是都是参考了Android-RTEditor的代码。再贴一下自己工程的链接:
https://github.com/InnerNight/rich-edit-text