Android 端图文混排( 富文本编辑器)粗体 斜体 下划线 中划线

紧跟上一篇Android 端 (图文混排)富文本编辑器的开发(一)
这一篇会对 Android 中的Span进行简单的介绍,并且会结合实际的需求对 span 进行应用,完成编辑器中粗体斜体下划线中划线 功能

1.Span介绍

1.1 应用Span

在使用Span时,经常会接触到SpannableStringSpannableStringBuilder 两个类。

这两个类的区别在于文本是否可变,类似于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[] getSpans(int start, int end, Class 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 start where a markup
     * object of class type begins or ends, or limit
     * if there are no starts or ends greater than start but less
     * than limit. Specify null 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 start…end
     * 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

这里需要区分几种情况

  1. 当前选中区域不存在 bold 样式 这里我们选中BB

    1. 1当前区域紧靠左侧或者右侧不存在粗体样式: AABBCC 这时候直接设置 span即可

    1.2当前区域紧靠左侧或者右侧存在粗体样式如: AABBCC AABBCC AABBCC

    这时候需要合并左右两侧的span,只剩下一个 span

  2. 当前选中区域存在了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[] getSpans(int start, int end, Class 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 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 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 还不止于此

我们当前的场景是 输入 ,用户可以随意的删除文本,在删除过程中可能会出现如下的情况:

  1. 用户输入了 AABBCCDD
  2. 用户选择了粗体样式 AABBCCDD
  3. 用户删除了CC然后显示如下 : AABB DD

这个时候选中其中的BD 此时,在该区域中 存在两个span ,并且没有一个 span 完全包裹选中的 BD

在这种情况下 仍需要进行 左右侧边界判断进行删除

那么就需要的上方的checkAndMergeSpan 方法中 增加 这种情况的判断

3.2取消样式实现
  1. 在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);
            }
        }
  1. 移除样式代码

    
        /**
         * @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 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 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 {

    protected Class clazzE;

    public NormalStyle() {
        clazzE = (Class) ((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 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 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 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 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 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  void removeAllSpans(Editable editable, int start, int end, Class clazzE) {
        E[] allSpans = editable.getSpans(start, end, clazzE);
        for (E span : allSpans) {
            editable.removeSpan(span);
        }
    }
}

实现类:

BoldStyle

public class BoldStyle extends NormalStyle {
    @Override
    protected BoldSpan newSpan() {
        return new BoldSpan();
    }
}

ItalicStyle

public class ItalicStyle extends NormalStyle {
    @Override
    protected ItalicSpan newSpan() {
        return new ItalicSpan();
    }
}

UnderlineStyle

public class UnderlineStyle extends NormalStyle {
    @Override
    protected UnderlineSpan newSpan() {
        return new UnderlineSpan();
    }
}

StriketgrougnStyle

public class StrikethroughStyle extends NormalStyle {
    @Override
    protected StrikethroughSpan newSpan() {
        return new StrikethroughSpan();
    }
}

下篇预告

下一篇将分析实现 字体大小、**对齐样式 **

你可能感兴趣的:(Android 端图文混排( 富文本编辑器)粗体 斜体 下划线 中划线)