用Span简单实现文本编辑器

使用

文本编辑器在APP中太常见了,但如何实现的呢?不知大家有没有跟我一个疑问?下面我将用Span来实现一个简单的文本编辑器。国际惯例,先上效果图。

文本编辑器演示.gif

怎样,效果是不是还行,使用也很简单,只要一行代码就能改变文本的样式!

1.添加依赖
compile 'com.leo.extendedittext:library:0.1.1'
2.布局中配置
<com.leo.extendedittext.ExtendEditText
    android:id="@+id/extend_edit_text"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:textSize="@dimen/normal_text_size"
    android:scrollbars="none"
    android:background="@android:color/transparent"
    app:bulletColor="@color/colorPrimary" // 着重号颜色
    app:bulletRadius="@dimen/bullet_radius" // 着重号半径
    app:bulletGapWidth="@dimen/bullet_gap_width" // 着重号与文本的宽度
    app:quoteColor="@color/colorPrimary" // 引用颜色
    app:quoteStripeWidth="@dimen/quote_stripe_width" // 引用宽度
    app:quoteGapWidth="@dimen/quote_gap_width" // 引用与文本的宽度
    app:linkColor="@color/colorPrimaryDark" // 链接颜色
    app:drawUnderLine="true" // 链接是否画下划线
    app:enableHistory="true" // 是否开启历史记录
    app:historyCapacity="50" // 历史记录容量
    app:rule="EXCLUSIVE_EXCLUSIVE"> // 规则,后面说
</com.leo.extendedittext.ExtendEditText>

当然,配置项也可以用代码设置,如:

mExtendEdt.enableHistory(true); // 开启历史记录
3.设置样式

配置好了就非常简单了,只要选中文本,调用相应的接口,所选文本就会更换样式。

- mExtendEdt.bold(); // 粗体
- mExtendEdt.italic(); // 斜体
- mExtendEdt.underline(); // 下划线
- mExtendEdt.strikethrough(); // 删除线
- mExtendEdt.link(); // 链接
- mExtendEdt.bullet(); // 着重号
- mExtendEdt.quote(); // 引用

细心的同学应该看到我上面的配置有个app:rule的配置项,这是设置更换样式的规则,也可以代码设置。

mExtendEdt.setRule(Rule.EXCLUSIVE_INCLUSIVE);

有下面四个规则作用分别如下:

- Rule.EXCLUSIVE_EXCLUSIVE  // 设置样式只对选中文本有影响
- Rule.EXCLUSIVE_INCLUSIVE  // 设置样式对选中的文本有影响, 并在其后输入的文本也会有该样式
- Rule.INCLUSIVE_EXCLUSIVE  // 设置样式对选中的文本有影响, 并在其前输入的文本也会有该样式
- Rule.INCLUSIVE_INCLUSIVE  // 设置样式对选中的文本有影响, 并在其前后输入的文本都会有该样式

是不是还是不太懂什么意思,我举个例子。例如我设置了EXCLUSIVE_INCLUSIVE的规则,当我给选中文本设置为粗体时,在选中文本后继续输入文本,新增的文本也会为粗体;而在刚选中的文本前输入文本呢,就是普通的文本样式。但经我测试,除了EXCLUSIVE_EXCLUSIVE 规则以外的三种规则都不好控制...

使用就这么简单了!其实我还实现了链式调用。但发现链式调用的场景不多,一般设置字体都是点击一个样式图标设置一种样式,所以链式调用就没多大用处了,看看就好。

mExtendEdt.cover()
          .bold()
          .italic()
          .underline()
          .strikethrough()
          .link()
          .bullet()
          .quote()
          .action();

原理

在讲解原理之前,各位同学需要对Span有一定的了解,可以看这篇文章:【译】Spans,一个强大的概念/#使用自定义的span

每种样式对应一个Span,例如粗体样式对应StyleSpan(Typeface.BOLD)、斜体对应StyleSpan(Typeface.ITALIC)、下划线对应UnderlineSpan,只要获取到相应的样式,再调用Spannable.setSpan接口来设置样式即可。

// what参数传Span对象
// start文本开始索引
// end文本结束索引
// flags有四个值,分别为
// Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
// Spanned.SPAN_EXCLUSIVE_INCLUSIVE
// Spanned.SPAN_INCLUSIVE_EXCLUSIVE
// Spanned.SPAN_INCLUSIVE_INCLUSIVE

public void setSpan(Object what, int start, int end, int flags);

聪明的你应该猜到了,flags对应的就是我上面所说的Rule,我只是封装一层而已。

这里可能大家有疑问,我怎么获取到Spannable呢?放心,Editable是继承于Spannable的!

public interface Editable extends CharSequence, GetChars, Spannable, Appendable {
    ...
}

是的,我们只要继承EditView来实现文本编辑器,就能获取到Editable,也就能对文本进行样式修改了。说到这里捋一捋实现文本编辑器的思路:

  1. 创建继承于EditText的View
  • 获取EditText的Editable对象
  • 调用Editable的setSpan来设置样式

怎样?思路是不是非常简单明了!但只要设置样式就够了吗?APP往往点击一个按钮设置样式,再点击一次就清除样式。考虑到这里,设置样式和清除样式应该是同一个接口比较合理。好,基于此再来捋一捋思路:

  1. 创建继承于EditText的View
  • 获取EditText的Editable对象
  • 判断选中文本是否具有将要设置样式的样式
  • 若已设置,清除样式
  • 若没设置,设置样式

基于上面的思路,我定义了一个Style抽象类,下面是核心代码:

public abstract class Style {

    /**
     * 改变选中文本样式
     * @param text 选中的可编辑文本
     * @param start 开始索引
     * @param end 结束索引
     * @param rule 规则
     * @return 若设置样式返回true, 清除样式返回false
     */
    public boolean format(Editable text, int start, int end, Rule rule) {
        ...
        boolean result = false;
        if (!isSetting(text, start, end)) {
            set(text, start, end);
            result = true;
        } else {
            remove(text, start, end);
        }

        return result;
    }

    /**
     * 设置样式
     * @param text 可编辑文本
     * @param start 开始索引
     * @param end 结束索引
     */
    public abstract void set(Editable text, int start, int end);

    /**
     * 移除样式
     * @param text 可编辑文本
     * @param start 开始索引
     * @param end 结束索引
     */
    public abstract void remove(Editable text,int start, int end);

    /**
     * 选中文本是否已设置样式
     * @param text 可编辑文本
     * @param start 开始索引
     * @param end 结束索引
     * @return 若选中的全部文本已设置该样式, 返回true; 反之, 返回false.
     */
    public abstract boolean isSetting(Editable text, int start, int end);

    ...

然后各种样式继承Style抽象类,并实现isSetting、set和remove方法即可。下面用粗体Bold类的实现代码:

public class Bold extends Style {

    @Override
    public void set(Editable text, int start, int end) {
        if (start >= end) {
            return;
        }

        text.setSpan(new StyleSpan(Typeface.BOLD), start, end, mRule);
    }

    @Override
    public void remove(Editable text, int start, int end) {
        if (start >= end) {
            return;
        }

        StyleSpan[] spans = text.getSpans(start, end, StyleSpan.class);
        List<TypeBean> list = new ArrayList<>(spans.length);
        for (StyleSpan span : spans) {
            if (span.getStyle() == Typeface.BOLD) {
                list.add(new TypeBean(text.getSpanStart(span), text.getSpanEnd(span)));
                text.removeSpan(span); // remove
            }
        }

        // 恢复未选上但与移除文本具有相同样式的文本
        for (TypeBean bean : list) {
            if (bean.isValid()) {
                if (bean.getStart() < start) {
                    set(text, bean.getStart(), start);
                }

                if (bean.getEnd() > end) {
                    set(text, end, bean.getEnd());
                }
            }
        }
    }

    @Override
    public boolean isSetting(Editable text, int start, int end) {
        if (start >= end) {
            return false;
        }

        // 思路: 遍历可编辑文本, 若选中文本存在未设置该样式的, 返回false; 反之, 返回true
        StringBuilder builder = new StringBuilder();
        for (int i = start; i < end; i++) {
            // 获取每个字符的样式, 可能有重复, 只需获取判断一次
            StyleSpan[] spans = text.getSpans(i, i + 1, StyleSpan.class);
            for (StyleSpan span : spans) {
                if (span.getStyle() == Typeface.BOLD) {
                    builder.append(text.subSequence(i, i + 1).toString());
                    break;
                }
            }
        }

        return text.subSequence(start, end).toString().equals(builder.toString());
    }
}

代码注释说得很清楚了,这里不多说,但有一点需要提醒下,清除样式的接口是:

public void removeSpan(Object what);

可以看到, 没有指定开始和结束索引的,它会清除具有该样式的所有相邻的文本的样式。即如果HelloWorld整个单词是粗体,如果你选中“ello”,调用removeSpan来清除粗体样式,会把HelloWorld整个单词的粗体样式都清除掉。所以要想只清除选中的“ello”,就要先把整个单词的粗体样式清除,再对非选中的文本进行样式恢复。

另外一点需要注意的是,对于“着重号”、“链接”等样式,Android自带的不能满足我们的需求,所以需要自己改下,具体不说了,看源码吧!

结论

目前支持样式:

  • 粗体
  • 斜体
  • 下划线
  • 删除线
  • 链接
  • 着重号
  • 引用

未来更新支持样式:

  • 图片
  • 背景色

参考:

写篇文章不容易~ 记得帮我点个喜欢或者Star哈


源码下载

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

推荐阅读更多精彩内容