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> 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&hellip;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

这里需要区分几种情况

  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> 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 还不止于此

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

  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<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();
    }
}

下篇预告

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

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