Android富文本编辑器(三):文字样式

对于富文本编辑器来说,除了插入图片,最重要的功能之一应该就是提供不同的文字样式了。其中主要包括:加粗、斜体、切换文字颜色等。与图片不同,一般来说我们对于文字样式的产品需求包括两个方面:

  1. 设置文字样式;
  2. 获取某段文字的当前样式;

先说设置,可以参考这篇文章:
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样式,那么我们进行下面三个步骤:

  1. 将A样式span调整为1-5个字符(原来是1-8);
  2. 将B样式span调整为16-20个字符(原来是12-20);
  3. 再将C样式span设置在5-15个字符上;

对照上述逻辑,再看for循环中的代码,就可以很好地理解了。大家也可以再对照代码来看,可以查看上面所贴链接。当然也可以在我的工程代码,其中只有BoldEffect,但是也有类似Effect的实现,但是都是参考了Android-RTEditor的代码。再贴一下自己工程的链接:
https://github.com/InnerNight/rich-edit-text

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容