對(duì)于富文本編輯器來(lái)說(shuō),除了插入圖片,最重要的功能之一應(yīng)該就是提供不同的文字樣式了。其中主要包括:加粗、斜體、切換文字顏色等。與圖片不同,一般來(lái)說(shuō)我們對(duì)于文字樣式的產(chǎn)品需求包括兩個(gè)方面:
- 設(shè)置文字樣式;
- 獲取某段文字的當(dāng)前樣式;
先說(shuō)設(shè)置,可以參考這篇文章:
http://hunankeda110.iteye.com/blog/1420470
//設(shè)置字體前景色
msp.setSpan(new ForegroundColorSpan(Color.MAGENTA), 12, 15, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //設(shè)置前景色為洋紅色
//設(shè)置字體背景色
msp.setSpan(new BackgroundColorSpan(Color.CYAN), 15, 18, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //設(shè)置背景色為青色
//設(shè)置字體樣式正常,粗體,斜體,粗斜體
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); //粗斜體
這個(gè)其實(shí)在之前本系列的文章中就介紹過(guò),是spannable string的基本用法。但是為了寫出擴(kuò)展性好、更工程化的方法,滿足修改、查找、添加等不同的需求,只有這個(gè)還是不行的。在網(wǎng)上找了很多源碼和資料后,向大家推薦這個(gè)項(xiàng)目的寫法:
https://github.com/1gravity/Android-RTEditor
這個(gè)項(xiàng)目實(shí)現(xiàn)的富文本編輯器非常強(qiáng)大,幾乎可以完成所有富文本的功能,筆者也從中獲益良多。這個(gè)項(xiàng)目中采用了類似工廠方法的模式,提供了一個(gè)Effect抽象基類,所有的文字樣式都繼承自該基類,并負(fù)責(zé)構(gòu)造對(duì)應(yīng)的Span,在基類中實(shí)現(xiàn)了apply方法,應(yīng)用不同的文字樣式。
如下,是一個(gè)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的實(shí)現(xiàn)如下:
public class BoldSpan extends StyleSpan implements Span<Boolean> {
public BoldSpan() {
super(Typeface.BOLD);
}
@Override
public Boolean getValue() {
return Boolean.TRUE;
}
}
下面我們看一下Effect的源碼,理解上面子類中g(shù)etSpanClazz和newSpan的應(yīng)用場(chǎng)景。首先是判斷當(dāng)前樣式在選定的文本中是否存在,代碼如下:
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中提供了實(shí)現(xiàn)。接口定義如下:
/**
* 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);
上面調(diào)用過(guò)程中,使用了子類中重載的getSpanClazz方法。
下面再來(lái)看看apply方法的實(shí)現(xiàn):
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)開(kāi)始,設(shè)置樣式span,其中用到了子類的newSpan方法。我們注意到,如果selection為空,則設(shè)置成Spanned.SPAN_INCLUSIVE_INCLUSIVE,而不為空是Spanned.SPAN_EXCLUSIVE_INCLUSIVE。一般來(lái)說(shuō),文字樣式應(yīng)該設(shè)置成SPAN_EXCLUSIVE_INCLUSIVE,這樣后續(xù)添加的文字就可以采用相同樣式。但在selection為空時(shí),前后都需要采用同樣的樣式,這樣再插入新的文字就可以達(dá)到該效果。
而前面for循環(huán)的部分,其實(shí)是在檢測(cè)之前的這段文字中是否已經(jīng)設(shè)置了樣式span。舉例說(shuō)明,如果一段文字長(zhǎng)度為20,其中1-8個(gè)字符已經(jīng)被設(shè)置了A樣式,12-20個(gè)字符已經(jīng)被設(shè)置了B樣式,現(xiàn)在要給5-15個(gè)字符設(shè)置C樣式,那么我們進(jìn)行下面三個(gè)步驟:
- 將A樣式span調(diào)整為1-5個(gè)字符(原來(lái)是1-8);
- 將B樣式span調(diào)整為16-20個(gè)字符(原來(lái)是12-20);
- 再將C樣式span設(shè)置在5-15個(gè)字符上;
對(duì)照上述邏輯,再看for循環(huán)中的代碼,就可以很好地理解了。大家也可以再對(duì)照代碼來(lái)看,可以查看上面所貼鏈接。當(dāng)然也可以在我的工程代碼,其中只有BoldEffect,但是也有類似Effect的實(shí)現(xiàn),但是都是參考了Android-RTEditor的代碼。再貼一下自己工程的鏈接:
https://github.com/InnerNight/rich-edit-text