紧跟上一篇Android 端 (图文混排)富文本编辑器的开发(一)
这一篇会对 Android 中的Span进行简单的介绍,并且会结合实际的需求对 span 进行应用,完成编辑器中粗体、斜体、下划线、中划线 功能
1.Span介绍
1.1 应用Span
在使用Span时,经常会接触到SpannableString 和 SpannableStringBuilder 两个类。
这两个类的区别在于文本是否可变,类似于String与StringBuilder之间的关系,SpannableStringBuilder可以修改文本内容。
SpannableString与SpannableStringBuilder 都实现了 Spannable接口,Spannable接口继承了Spanned 接口。
先看 Spanned接口重要的方法,方法上 加了中文注释:
/**
* 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.
*/
//---获取 从 start 到 end 位置上所有的指定 class 类型的 Span数组
public <T> T[] getSpans(int start, int end, Class<T> type);
/**
* Return the beginning of the range of text to which the specified
* markup object is attached, or -1 if the object is not attached.
*/
//获取 一个 span 的起始位置
public int getSpanStart(Object tag);
/**
* Return the end of the range of text to which the specified
* markup object is attached, or -1 if the object is not attached.
*/
//获取一个span 的结束位置
public int getSpanEnd(Object tag);
/**
* Return the first offset greater than <code>start</code> where a markup
* object of class <code>type</code> begins or ends, or <code>limit</code>
* if there are no starts or ends greater than <code>start</code> but less
* than <code>limit</code>. Specify <code>null</code> or Object.class for
* the type if you want every transition regardless of type.
*/
// 在指定的文本范围内,返回下一个 指定 class 类型的 span开始
public int nextSpanTransition(int start, int limit, Class type);
接下来看 Spannable 接口方法:
/**
* Attach the specified markup object to the range <code>start…end</code>
* of the text, or move the object to that range if it was already
* attached elsewhere. See {@link Spanned} for an explanation of
* what the flags mean. The object can be one that has meaning only
* within your application, or it can be one that the text system will
* use to affect text display or behavior. Some noteworthy ones are
* the subclasses of {@link android.text.style.CharacterStyle} and
* {@link android.text.style.ParagraphStyle}, and
* {@link android.text.TextWatcher} and
* {@link android.text.SpanWatcher}.
*/
//----设置 span 这里的 what 指的是 span 对象
// 从 start 到 end 位置 设置 span 样式
// flags 为 Spanned中的变量,接下来会分析到
public void setSpan(Object what, int start, int end, int flags);
/**
* Remove the specified object from the range of text to which it
* was attached, if any. It is OK to remove an object that was never
* attached in the first place.
*/
// 在spannable 中移出指定的 span
public void removeSpan(Object what);
上面四种 flags
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括);
- Spanned.SPAN_INCLUSIVE_EXCLUSIVE(前面包括,后面不包括);
- Spanned.SPAN_EXCLUSIVE_INCLUSIVE(前面不包括,后面包括);
- Spanned.SPAN_INCLUSIVE_INCLUSIVE(前后都包括)。
1.2 Span 的分类
1.影响字符级别
这一类型的span 作用范围是 字符级别,通过设置 TextPaint来影响字符的外观,大小等。
1.字符外观
这种类型修改字符的外形但是不影响字符的测量,会触发文本重新绘制但是不触发重新布局。
常见的
BackgroundColorSpan
var str1 = SpannableString("测试BackgroundColorSpan使用")
str1.setSpan(BackgroundColorSpan(Color.GREEN), 2, str1.length - 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_background.setText(str1)
ForegroundColorSpan
var str2 = SpannableString("测试ForegroundColorSpan使用")
str2.setSpan(ForegroundColorSpan(Color.RED), 2, str2.length - 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_forground.setText(str2)
UnderlineSpan
var str3 = SpannableString("测试UnderlineSpan使用")
str3.setSpan(UnderlineSpan(), 2, str3.length - 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_underline.setText(str3)
StrikethrougnSpan
var str4 = SpannableString("测试StrikethrougnSpan使用")
str4.setSpan(StrikethroughSpan(), 2, str4.length - 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_strikethrough.setText(str4)
2.字符大小布局
这种类型Span会更改文本的大小和布局,会触发文本的重新测量绘制
常见的
StyleSpan
var str5 = SpannableString("测试StyleSpan使用")
str5.setSpan(StyleSpan(Typeface.BOLD),2,7,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
str5.setSpan(StyleSpan(Typeface.ITALIC),7,str5.length-2,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_style.setText(str5)
RelativeSizeSpan
var str6 = SpannableString("测试 RelativeSizeSpan 使用")
str6.setSpan(RelativeSizeSpan(1.5f),2,str6.length-2,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_relactive.setText(str6)
AbsoluteSizeSpan
var str7 = SpannableString("测试 AbsoluteSizeSpan 使用")
str7.setSpan(AbsoluteSizeSpan(30,true),2,str7.length-2,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_absolute.setText(str7)
特殊说明:
RelativeSizeSpan 设置文字相对大小,指相对于文本设定的大小的相对比例。
AbsoluteSizeSpan 设置文字绝对大小。
TypefaceSpan
var str8 = SpannableString("测试TypefaceSpan使用")
str8.setSpan(TypefaceSpan("serif"),2,9,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_typeface.setText(str8)
2.影响段落级别
这种类型Span 在段落级别起作用,更改文本块在段落级别的外观,修改对齐方式,边距等。
在Android 中,段落是基于换行符 **\n ** 定义的
字符级别的Span 作用于当前段落丢一个字符到当前段落的最后一个字符
常见的
AlignmentSpan
居中:
var str9 = "测试换行段落级span 使用\n这是换行后的内容"
var aligmentCenter = SpannableString(str9) aligmentCenter.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER),0,aligmentCenter.length,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_alignmant_center.setText(aligmentCenter)
右对齐:
var aligmentRight = SpannableString(str9)
aligmentRight.setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE),0,aligmentRight.length,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_alignmant_right.setText(aligmentRight)
BulletSpan
var bullet = SpannableString(str9)
bullet.setSpan(BulletSpan(30,Color.BLUE),0,bullet.length,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_bullet.setText(bullet)
QuoteSpan
var quote = SpannableString(str9)
quote.setSpan(QuoteSpan(),0,quote.length,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
tv_quote.setText(quote)
2. 实战使用
在以上部分已经分析了 span的基本用法,接下来将结合实际项目需求进一步学习span 的使用
这里的实战项目是 Android 端 富文本编辑器,没有看上一篇的可以先去看一看项目整体的效果。
在这篇文章中,将实现基础样式 粗体、 斜体、 下划线 、中划线 的设置和取消
EditText
在这里要说明一下 Android中的 EditText 支持span 输入
/**
* Return the text that TextView is displaying as an Editable object. If the text is not
* editable, null is returned.
*
* @see #getText
*/
public Editable getEditableText() {
return (mText instanceof Editable) ? (Editable) mText : null;
}
//-------------------------------------------------
// EditText 的输入区域Editable 继承了Spannable
public interface Editable
extends CharSequence, GetChars, Spannable, Appendable{
...
}
再说明两个EditText 的 api
getSelectionStart() //获取当前选中的起始位置
getSelectionEnd() //获取当前选中的末尾位置
粗体
1. 创建 span对象
public class BoldSpan extends StyleSpan {
public BoldSpan() {
super(Typeface.BOLD);
}
}
2.设置Span
这里需要区分几种情况
-
当前选中区域不存在 bold 样式 这里我们选中BB
- 1当前区域紧靠左侧或者右侧不存在粗体样式: AABBCC 这时候直接设置 span即可
1.2当前区域紧靠左侧或者右侧存在粗体样式如: AABBCC AABBCC AABBCC
这时候需要合并左右两侧的span,只剩下一个 span
-
当前选中区域存在了Bold 样式 选中 ABBC
四种情况:
选中样式两侧不存在连续的bold样式 AABBCC
选中内部两端存在连续的bold 样式 AABBCC
选中左侧存在连续的bold 样式 AABBCC
选中右侧存在连续的bold 样式 AABBCC
这时候需要合并左右两侧已经存在的span,只剩下一个 span
2.1 情况判断
BoldSpan [] spans = editable.getSpans(start, end, BoldSpan.class);
BoldSpan existingSpan = null;
if (spans.length > 0) {
existingSpan = spans[0];
}
if (existingSpan == null) {
//当前选中 内部无Bold样式
} else {
int existingESpanStart = editable.getSpanStart(existingSpan);
int existingESpanEnd = editable.getSpanEnd(existingSpan);
if (existingESpanStart <= start && existingESpanEnd >= end) {
// 当前选中的 区域 在一个完整的 span 中
//这里需要 取消span
} else {
//当前选中区域存在了bold 样式
}
}
2.2 边界判断与设置
2.2.1 判断左右侧是否存在span
根据 Spannable接口的api getSpans 方法:
/**
* 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.
*/
//---获取 从 start 到 end 位置上所有的指定 class 类型的 Span数组
public <T> T[] getSpans(int start, int end, Class<T> type);
可以获取指定位置所有span,我们重新设置 start 与end 来获取 左右侧span
//获取左侧 span
BoldSpan leftSpan = null;
int leftStart =start;
if(start>1){
leftStart = start-1;
}
BoldSpan [] leftSpans = editable.getSpans(leftStart, start, BoldSpan.class);
if (leftSpans.length > 0) {
leftSpan = leftSpans[0];
}
//获取右侧 span
int rightEnd;
if(end<editable.length()-1){
rightEnd =end+1;
}
BoldSpan rightSpan = null;
BoldSpan [] rightSpans = editable.getSpans(end, rightEnd, BoldSpan.class);
if (rightSpans.length > 0) {
rightSpan = rightSpans[0];
}
接下来进行设置 span,代码合并如下:
//-------------参数说明-----------
// start 选择起始位置
//end 选择末尾位置
private void checkAndMergeSpan(Editable editable, int start, int end) {
//获取左侧是否存在 span
BoldSpan leftSpan = null;
int leftStart =start;
if(start>1){
leftStart = start-1;
}
BoldSpan [] leftSpans = editable.getSpans(leftStart, start, BoldSpan.class);
if (leftSpans.length > 0) {
leftSpan = leftSpans[0];
}
//判断右侧是否存在 span
int rightEnd;
if(end<editable.length()-1){
rightEnd =end+1;
}
BoldSpan rightSpan = null;
BoldSpan [] rightSpans = editable.getSpans(end, rightEnd, BoldSpan.class);
if (rightSpans.length > 0) {
rightSpan = rightSpans[0];
}
//获取 两侧的 起始与 结束位置
int leftSpanStart = editable.getSpanStart(leftSpan);
int leftSpanEnd = editable.getSpanEnd(leftSpan);
int rightStart = editable.getSpanStart(rightSpan);
int rightSpanEnd = editable.getSpanEnd(rightSpan);
//先移除所有的 span
//如果 左右侧已经存在了 span 会一并删除
removeAllSpans(editable, start, end);
//-------------------------------------------------------------
if (leftSpan != null && rightSpan != null) {
//左右侧都存在了 span 合并 span
//新的 span 范围为: leftSpanStart - rightSpanEnd
BoldSpan eSpan = newSpan();
editable.setSpan(eSpan, leftSpanStart, rightSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (leftSpan != null && rightSpan == null) {
//左侧存在 span 右侧不存在
//新的 span 的范围为:leftSpanStart - end
BoldSpan eSpan = newSpan();
editable.setSpan(eSpan, leftSpanStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (leftSpan == null && rightSpan != null) {
//右侧存在 span 左侧不存在
//新的 span 范围为:start - rightSpanEnd
BoldSpan eSpan = newSpan();
editable.setSpan(eSpan, start, rightSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
//左右两边都不存在 span
// span 范围为:start - end
BoldSpan eSpan = newSpan();
editable.setSpan(eSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
/**
* 创建 新 span
*/
protected BoldSpan newSpan(){
return new BoldSpan();
}
private BoldSpan void removeAllSpans(Editable editable, int start, int end) {
BoldSpan [] allSpans = editable.getSpans(start, end, BoldSpan.class);
for (E span : allSpans) {
editable.removeSpan(span);
}
}
3.取消Span
上一章分析了如何设置粗体 BoldSpan 样式,这一章将分析如何取消BoldSpan样式
3.1什么时候取消样式
当我们选中的区域在一段连续的 Bold 样式里面的时候,再次选择Bold将会取消样式
还记得在 2.1 章节里面分析:
int existingESpanStart = editable.getSpanStart(existingSpan);
int existingESpanEnd = editable.getSpanEnd(existingSpan);
if (existingESpanStart <= start && existingESpanEnd >= end) {
// 当前选中的 区域 在一个完整的 span 中
//这里需要 取消span
} else {
//当前选中区域存在了bold 样式
}
这里分析了一种情况 当前选中区域存在了一个span 并且我们选中的区域 完全包含在 该 span 中,这时候就需要移除 span 效果
你以为到这里移除样式已经结束了吗? no no no 还不止于此
我们当前的场景是 输入 ,用户可以随意的删除文本,在删除过程中可能会出现如下的情况:
- 用户输入了 AABBCCDD
- 用户选择了粗体样式 AABBCCDD
- 用户删除了CC然后显示如下 : AABB DD
这个时候选中其中的BD 此时,在该区域中 存在两个span ,并且没有一个 span 完全包裹选中的 BD
在这种情况下 仍需要进行 左右侧边界判断进行删除
那么就需要的上方的checkAndMergeSpan 方法中 增加 这种情况的判断
3.2取消样式实现
-
在checkAndMergeSpan 方法中添加代码
上述特殊情况出现在 左右两侧存在 span 并且 左侧span 的end 与右侧 span 的start 相等
if (leftSpan != null && rightSpan != null) {
if (leftSpanEnd == rightStart) {
//选中的两端是 连续的 样式
//执行删除 样式
//false 表示不在一个完整的 span 中
removeStyle(editable, start, end, false);
} else {
BoldSpan eSpan = newSpan();
editable.setSpan(eSpan, leftSpanStart, rightSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
-
移除样式代码
/** * @param editable * @param start 当前选中起始位置 * @param end 当前选中末尾位置 * @param isSame 是否在 同一个 span 内部 */ private void removeStyle(Editable editable, int start, int end, boolean isSame) { BoldSpan [] spans = editable.getSpans(start, end, BoldSpan.class); if (spans.length > 0) { if (isSame) { //在 同一个 span 中 E span = spans[0]; if (null != span) { // 已经存在 span 的 start int ess = editable.getSpanStart(span); // 已经存在 span 的 end int ese = editable.getSpanEnd(span); if (start == ess && end == ese) { // *BBBBBB* // 完全选择 直接移除 span editable.removeSpan(span); } else if (start > ess && end < ese) { // // BB*BB*BB // *BB* 选中中间的部分 则移除span 并创建两个新的 span editable.removeSpan(span); E spanLeft = newSpan(); editable.setSpan(spanLeft, ess, start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); E spanRight = newSpan(); editable.setSpan(spanRight, end, ese, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (start == ess && end < ese) { // // *BBBB*BB // *BBBB* 选中起始位置 移除span 并创建新的span 范围为 end-ese editable.removeSpan(span); E newSpan = newSpan(); editable.setSpan(newSpan, end, ese, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else if (start > ess && end == ese) { // // BB*BBBB* // *BBBB* 选中末尾位置 移除span 并创建新的span 范围为 ess - start editable.removeSpan(span); E newSpan = newSpan(); editable.setSpan(newSpan, ess, start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } else { //---------------------------------- //这里的代码 针对特殊情况 Pair<BoldSpan, BoldSpan> firstAndLast = findFirstAndLast(editable, spans); BoldSpan firstSpan = firstAndLast.first; BoldSpan lastSpan = firstAndLast.second; //获取左侧 span 起始位置 int leftStart = editable.getSpanStart(firstSpan); //获取span 结束位置 int rightEnd = editable.getSpanEnd(lastSpan); //移除左侧span editable.removeSpan(firstSpan); //当左侧span 超出了选择 边界 //保留超出部分 if (start != leftStart) { editable.setSpan(firstSpan, leftStart, start, Spanned.SPAN_EXCLUSIVE_INCLUSIVE); } //移除右侧span editable.removeSpan(lastSpan); //当右侧span 超出了选择 边界 //保留超出部分 if (rightEnd != end) { editable.setSpan(lastSpan, end, rightEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } } } //这里特殊说明 由于 通过spans[0]不能获取到最左侧 span //所以这里需要获取 最左侧 span 和 最右侧 span public Pair<BoldSpan, BoldSpan> findFirstAndLast(Editable editable, BoldSpan [] targetSpans) { E firstTargetSpan = targetSpans[0]; E lastTargetSpan = targetSpans[0]; if (targetSpans.length > 0) { int firstTargetSpanStart = editable.getSpanStart(firstTargetSpan); int lastTargetSpanEnd = editable.getSpanEnd(firstTargetSpan); for (E lns : targetSpans) { int lnsStart = editable.getSpanStart(lns); int lnsEnd = editable.getSpanEnd(lns); if (lnsStart < firstTargetSpanStart) { firstTargetSpan = lns; firstTargetSpanStart = lnsStart; } if (lnsEnd > lastTargetSpanEnd) { lastTargetSpan = lns; lastTargetSpanEnd = lnsEnd; } } } return new Pair(firstTargetSpan, lastTargetSpan); }
斜体、 下划线 、中划线
通过上述的分析我们已经实现了 粗体 BoldSpan 样式的设置和取消
斜体、 下划线 、中划线 样式的设置和取消与粗体样式一致,只是创建 span 的区别而已,可以将代码进行抽取
抽象类 NormalStyle
public abstract class NormalStyle<E> {
protected Class<E> clazzE;
public NormalStyle() {
clazzE = (Class<E>) ((ParameterizedType) this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
public void applyStyle(Editable editable, int start, int end) {
E[] spans = editable.getSpans(start, end, clazzE);
E existingSpan = null;
if (spans.length > 0) {
existingSpan = spans[0];
}
if (existingSpan == null) { //当前选中 内部无此样式
checkAndMergeSpan(editable, start, end, clazzE);
} else {
int existingESpanStart = editable.getSpanStart(existingSpan);
int existingESpanEnd = editable.getSpanEnd(existingSpan);
if (existingESpanStart <= start && existingESpanEnd >= end) {
//在一个 完整的 span 中
//删除 样式
removeStyle(editable, start, end, clazzE, true);
} else {
checkAndMergeSpan(editable, start, end, clazzE);
}
}
}
/**
* @param editable
* @param start
* @param end
* @param clazzE
* @param isSame 是否在 同一个 span 内部
*/
private void removeStyle(Editable editable, int start, int end, Class<E> clazzE, boolean isSame) {
E[] spans = editable.getSpans(start, end, clazzE);
if (spans.length > 0) {
if (isSame) {
//在 同一个 span 中
E span = spans[0];
if (null != span) {
//
// User stops the style, and wants to show
// un-UNDERLINE characters
int ess = editable.getSpanStart(span); // ess == existing span start
int ese = editable.getSpanEnd(span); // ese = existing span end
if (start >= ese) {
// User inputs to the end of the existing e span
// End existing e span
editable.removeSpan(span);
editable.setSpan(span, ess, start - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (start == ess && end == ese) {
// Case 1 desc:
// *BBBBBB*
// All selected, and un-showTodo e
editable.removeSpan(span);
} else if (start > ess && end < ese) {
// Case 2 desc:
// BB*BB*BB
// *BB* is selected, and un-showTodo e
editable.removeSpan(span);
E spanLeft = newSpan();
editable.setSpan(spanLeft, ess, start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
E spanRight = newSpan();
editable.setSpan(spanRight, end, ese, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (start == ess && end < ese) {
// Case 3 desc:
// *BBBB*BB
// *BBBB* is selected, and un-showTodo e
editable.removeSpan(span);
E newSpan = newSpan();
editable.setSpan(newSpan, end, ese, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (start > ess && end == ese) {
// Case 4 desc:
// BB*BBBB*
// *BBBB* is selected, and un-showTodo e
editable.removeSpan(span);
E newSpan = newSpan();
editable.setSpan(newSpan, ess, start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else {
Pair<E, E> firstAndLast = findFirstAndLast(editable, spans);
E firstSpan = firstAndLast.first;
E lastSpan = firstAndLast.second;
int leftStart = editable.getSpanStart(firstSpan);
int rightEnd = editable.getSpanEnd(lastSpan);
editable.removeSpan(firstSpan);
if (start != leftStart) {
editable.setSpan(firstSpan, leftStart, start, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
editable.removeSpan(lastSpan);
if (rightEnd != end) {
editable.setSpan(lastSpan, end, rightEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
public Pair<E, E> findFirstAndLast(Editable editable, E[] targetSpans) {
E firstTargetSpan = targetSpans[0];
E lastTargetSpan = targetSpans[0];
if (targetSpans.length > 0) {
int firstTargetSpanStart = editable.getSpanStart(firstTargetSpan);
int lastTargetSpanEnd = editable.getSpanEnd(firstTargetSpan);
for (E lns : targetSpans) {
int lnsStart = editable.getSpanStart(lns);
int lnsEnd = editable.getSpanEnd(lns);
if (lnsStart < firstTargetSpanStart) {
firstTargetSpan = lns;
firstTargetSpanStart = lnsStart;
}
if (lnsEnd > lastTargetSpanEnd) {
lastTargetSpan = lns;
lastTargetSpanEnd = lnsEnd;
}
}
}
return new Pair(firstTargetSpan, lastTargetSpan);
}
private void checkAndMergeSpan(Editable editable, int start, int end, Class<E> clazzE) {
//获取左侧是否存在 span
BoldSpan leftSpan = null;
int leftStart =start;
if(start>1){
leftStart = start-1;
}
BoldSpan [] leftSpans = editable.getSpans(leftStart, start, BoldSpan.class);
if (leftSpans.length > 0) {
leftSpan = leftSpans[0];
}
//判断右侧是否存在 span
int rightEnd;
if(end<editable.length()-1){
rightEnd =end+1;
}
BoldSpan rightSpan = null;
BoldSpan [] rightSpans = editable.getSpans(end, rightEnd, BoldSpan.class);
if (rightSpans.length > 0) {
rightSpan = rightSpans[0];
}
int leftSpanStart = editable.getSpanStart(leftSpan);
int leftSpanEnd = editable.getSpanEnd(leftSpan);
int rightStart = editable.getSpanStart(rightSpan);
int rightSpanEnd = editable.getSpanEnd(rightSpan);
removeAllSpans(editable, start, end, clazzE);
if (leftSpan != null && rightSpan != null) {
if (leftSpanEnd == rightStart) {
//选中的两端是 连续的 样式
removeStyle(editable, start, end, clazzE, false);
} else {
E eSpan = newSpan();
editable.setSpan(eSpan, leftSpanStart, rightSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else if (leftSpan != null && rightSpan == null) {
E eSpan = newSpan();
editable.setSpan(eSpan, leftSpanStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (leftSpan == null && rightSpan != null) {
E eSpan = newSpan();
editable.setSpan(eSpan, start, rightSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
E eSpan = newSpan();
editable.setSpan(eSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
protected abstract E newSpan();
private <E> void removeAllSpans(Editable editable, int start, int end, Class<E> clazzE) {
E[] allSpans = editable.getSpans(start, end, clazzE);
for (E span : allSpans) {
editable.removeSpan(span);
}
}
}
实现类:
BoldStyle
public class BoldStyle extends NormalStyle<BoldSpan> {
@Override
protected BoldSpan newSpan() {
return new BoldSpan();
}
}
ItalicStyle
public class ItalicStyle extends NormalStyle<ItalicSpan> {
@Override
protected ItalicSpan newSpan() {
return new ItalicSpan();
}
}
UnderlineStyle
public class UnderlineStyle extends NormalStyle<UnderlineSpan> {
@Override
protected UnderlineSpan newSpan() {
return new UnderlineSpan();
}
}
StriketgrougnStyle
public class StrikethroughStyle extends NormalStyle<StrikethroughSpan> {
@Override
protected StrikethroughSpan newSpan() {
return new StrikethroughSpan();
}
}
下篇预告
下一篇将分析实现 字体大小、**对齐样式 **