使用
文本编辑器在APP中太常见了,但如何实现的呢?不知大家有没有跟我一个疑问?下面我将用Span来实现一个简单的文本编辑器。国际惯例,先上效果图。
怎样,效果是不是还行,使用也很简单,只要一行代码就能改变文本的样式!
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,也就能对文本进行样式修改了。说到这里捋一捋实现文本编辑器的思路:
- 创建继承于EditText的View
- 获取EditText的Editable对象
- 调用Editable的setSpan来设置样式
怎样?思路是不是非常简单明了!但只要设置样式就够了吗?APP往往点击一个按钮设置样式,再点击一次就清除样式。考虑到这里,设置样式和清除样式应该是同一个接口比较合理。好,基于此再来捋一捋思路:
- 创建继承于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哈