Android SpannableString浅析

引言

       在应用程序开发过程经常需要对文本进行处理,比如说对一段描述文字的其中一段加入点击事件,或者对其设置不一样的前景色,有什么方法可以实现要求的功能呐?

需求样例

       比如我们需要实现如下图所示的功能,将文本:#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview) 处理成第二种或者第三种的形式。

实现方案

       根据上图,我们可以采用如下的方法来实现上诉要求的效果。

方案1

       比如显示效果二你可以能会说,我们可以采用三个TextView来实现,第一个TextView设置不一样的颜色,第二个正常显示内容,第三个处理点击事件。该方式对图二可能是能够实现的,但是如果第二行里面就有部分内容需要进行点击处理,就比较难以实现了。

       对于图三的效果上述的方式就很难实现了。必须要对TextView的内容进行处理了!!

方案2

       如果文案的处理只是简单的对齐,颜色,大小的变换,我们还可以采用自定义view来实现,在前面的文章中我们就采用了自定义view来显示了一个文字的排版效果,具体实现可以查看Android文本排版实现;

方案3

       除了上面的方案,我们还可以采用另外一个种方式来实现,采用html来显示,可以将要显示的内容转换成html的格式,用TextView来进行加载。说了这么多,我们来看看代码吧!

private void setText() {
    String originText = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview)";

    String effect1 = "<font color='#FF0000'>#重磅消息#</font> <br> 近日谷歌放出Android " +
            "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";

    String effect2 = "<font color='#303F9F'>#重磅消息#</font> 近日谷歌放出Android " +
            "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>";
    StringBuilder sb = new StringBuilder(originText);
    sb.append("<br><br><br><br>");
    sb.append(effect1);
    sb.append("<br><br><br><br>");
    sb.append(effect2);
    textView.setText(Html.fromHtml(sb.toString()));
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

       写到这,突然发现要跑题,仅仅是Html的实现就可以分析出很多的知识点,不过这里还是先契合主题,先这里挖一个坑,后续对html进行分析,Android Html
解析

方案4

       终于回到我们的主题了,这里我们采用SpannableString来实现上述的效果。代码如下:


private void setSpan() {
    String originText = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview)";

    SpannableStringBuilder sb = new SpannableStringBuilder(originText);
    sb.append("\r\n").append("\r\n").append("\r\n");
    getEffect1Span(sb);
    sb.append("\r\n").append("\r\n").append("\r\n");
    getEffect2Span(sb);
    textView.setText(sb);
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}

private void getEffect1Span(SpannableStringBuilder sb) {
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),
            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(span);
    sb.append("\n");
    String source2 = "近日谷歌放出Android N的第二个开发者预览版";
    sb.append(source2);

    final String source3 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source3);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source3);
        }
    }, 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(clickSpan);
}

private void getEffect2Span(SpannableStringBuilder sb) {
    String source1 = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimaryDark)), 0, 6, Spanned
            .SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(span);

    final String source2 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source2);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source2);
        }
    }, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    sb.append(clickSpan);
}

       上述代码采用了硬编码方式实现,正常实现,需要根据需求进行设置。记得要添加textView.setMovementMethod(LinkMovementMethod.getInstance());来接受点击事件。

SpnnableString详解

       SpannableString继承了SpannableStringInternal,同时实现了CharSequence, GetChars, Spannable三个接口,正常处理文本的函数为setSpan函数:

public void setSpan(Object what, int start, int end, int flags) {
    super.setSpan(what, start, end, flags);
}

       该函数有四个参数,第一个为一个span类型,第二个参数为开始位置,第三个位置为span的结束位置,最后一个为flag参数。
       what可以设置如下类型:

1, AbsoluteSizeSpan 设置文字字体的绝对大小, 有两个参数,第一个是字体大小,第二个是单位是否是dip

public AbsoluteSizeSpan(int size, boolean dip) {
        mSize = size;
        mDip = dip;
    }

2,AlignmentSpan 主要设置文本的对齐方式,有三种方式正常,居中,相反的方式对齐,默认实现为Standard

   public Standard(Layout.Alignment align) {
        mAlignment = align;
    }

3,BackgroundColorSpan 设置文字的背景色

private void setfCS(){
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new BackgroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(span);
}

4,BulletSpan 给文本的开始处加上项目符号。比如前面加一个 .

private void setBSpan() {
    final String source3 = "近日谷歌放出Android N的第二个开发者预览版";
    SpannableString bSpan = new SpannableString(source3);
    bSpan.setSpan(new BulletSpan(), 0, source3.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(bSpan);
}

5, ClickableSpan 设置文本的点击事件,要实现onClick函数,可以复写updateDrawState,设置下划线,或者取消下划线,还可以设置下划线颜色

private void setCS(){
    final String source2 = "(Developer Preview)";
    SpannableString clickSpan = new SpannableString(source2);
    clickSpan.setSpan(new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            ToastUtil.showLong(source2);
        }
    }, 0, source2.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(clickSpan);
}

6,DrawableMarginSpan 可以设置一个图标,并且可以设置与文字的宽度

private void setDMSpan() {
    final String source3 = "(Developer Preview)";
    SpannableString dmSpan = new SpannableString(source3);
    dmSpan.setSpan(new DrawableMarginSpan(getResources().getDrawable(R.mipmap.ic_launcher), 30), 0, source3
            .length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(dmSpan);
}

7,DynamicDrawableSpan 设置某段文字被图标替换,需要返回一个drawable

8,EasyEditSpan 当文本改变或者删除时调用, 例如入下长按可以很容易删除一行

private void setEdit() {
    editText.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
    editText.setSingleLine(false);
    editText.setText("近日\n谷歌放出Android N的\n第二个开发者预览版");
    editText.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View v) {
            final Layout layout = editText.getLayout();
            final int line = layout.getLineForOffset(editText.getSelectionStart());
            final int start = layout.getLineStart(line);
            final int end = layout.getLineEnd(line);
            editText.getEditableText().setSpan(new EasyEditSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            return true;
        }
    });
}

9,ForegroundColorSpan 设置文字前景色

private void setfCS(){
    String source1 = "#重磅消息#";
    SpannableString span = new SpannableString(source1);
    span.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorAccent)), 0, source1.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    textView.setText(span);
}

        写到这里我停下来了。天啦噜,30多个span,可以去系统代码package android.text.style包下查看,这么多,整个人都不好了。

Android SpannableString浅析_第1张图片
       因此先就针对上面的做了部分样例,之后会专门实现一下每个span的效果。仔细理解一个就行,其他的都是类似的,我们继续看看后面的参数。

       第二参数start和第三个参数end,表示当时设置的span作用效果的范围,start表示开始位置,end表示结束位置,第四个参数是一个flag标签。这里主要设置以下的值:

/** * Non-0-length spans of type SPAN_INCLUSIVE_EXCLUSIVE expand * to include text inserted at their starting point but not at their * ending point. When 0-length, they behave like marks. */
public static final int SPAN_INCLUSIVE_EXCLUSIVE = SPAN_MARK_MARK;

/** * Spans of type SPAN_INCLUSIVE_INCLUSIVE expand * to include text inserted at either their starting or ending point. */
public static final int SPAN_INCLUSIVE_INCLUSIVE = SPAN_MARK_POINT;

/** * Spans of type SPAN_EXCLUSIVE_EXCLUSIVE do not expand * to include text inserted at either their starting or ending point. * They can never have a length of 0 and are automatically removed * from the buffer if all the text they cover is removed. */
public static final int SPAN_EXCLUSIVE_EXCLUSIVE = SPAN_POINT_MARK;

/** * Non-0-length spans of type SPAN_EXCLUSIVE_INCLUSIVE expand * to include text inserted at their ending point but not at their * starting point. When 0-length, they behave like points. */
public static final int SPAN_EXCLUSIVE_INCLUSIVE = SPAN_POINT_POINT;

       常用的就是上述的四个值,这里我们来分别解释以下:
1. SPAN_INCLUSIVE_EXCLUSIVE表示左闭右开区间 “[ )”
2. SPAN_INCLUSIVE_INCLUSIVE表示左右都是闭区间 ‘( )’
3. SPAN_EXCLUSIVE_EXCLUSIVE表示左右都是闭区间 ‘[ ]’
4. SPAN_EXCLUSIVE_INCLUSIVE表示左右都是闭区间 ‘( ]’

       我们继续来看代码,SpannableString的setSpan又继续调用了SpannableStringInternal的setSpan函数。

/* package */ void setSpan(Object what, int start, int end, int flags) {
    int nstart = start;
    int nend = end;

    checkRange("setSpan", start, end);

    if ((flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH) {
        if (start != 0 && start != length()) {
            char c = charAt(start - 1);

            if (c != '\n')
                throw new RuntimeException(
                        "PARAGRAPH span must start at paragraph boundary" +
                        " (" + start + " follows " + c + ")");
        }

        if (end != 0 && end != length()) {
            char c = charAt(end - 1);

            if (c != '\n')
                throw new RuntimeException(
                        "PARAGRAPH span must end at paragraph boundary" +
                        " (" + end + " follows " + c + ")");
        }
    }

    int count = mSpanCount;
    Object[] spans = mSpans;
    int[] data = mSpanData;

    for (int i = 0; i < count; i++) {
        if (spans[i] == what) {
            int ostart = data[i * COLUMNS + START];
            int oend = data[i * COLUMNS + END];

            data[i * COLUMNS + START] = start;
            data[i * COLUMNS + END] = end;
            data[i * COLUMNS + FLAGS] = flags;

            sendSpanChanged(what, ostart, oend, nstart, nend);
            return;
        }
    }

    if (mSpanCount + 1 >= mSpans.length) {
        Object[] newtags = ArrayUtils.newUnpaddedObjectArray(
                GrowingArrayUtils.growSize(mSpanCount));
        int[] newdata = new int[newtags.length * 3];

        System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
        System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);

        mSpans = newtags;
        mSpanData = newdata;
    }

    mSpans[mSpanCount] = what;
    mSpanData[mSpanCount * COLUMNS + START] = start;
    mSpanData[mSpanCount * COLUMNS + END] = end;
    mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
    mSpanCount++;

    if (this instanceof Spannable)
        sendSpanAdded(what, nstart, nend);
}

/* package */ void removeSpan(Object what) {
    int count = mSpanCount;
    Object[] spans = mSpans;
    int[] data = mSpanData;

    for (int i = count - 1; i >= 0; i--) {
        if (spans[i] == what) {
            int ostart = data[i * COLUMNS + START];
            int oend = data[i * COLUMNS + END];

            int c = count - (i + 1);

            System.arraycopy(spans, i + 1, spans, i, c);
            System.arraycopy(data, (i + 1) * COLUMNS,
                             data, i * COLUMNS, c * COLUMNS);

            mSpanCount--;

            sendSpanRemoved(what, ostart, oend);
            return;
        }
    }
}

       首先调用了checkRange,判断了位置的合法性,如果start小于end,或者位置下标越界都会抛出IndexOutOfBoundsException异常。

       之后判断了(flags & Spannable.SPAN_PARAGRAPH) == Spannable.SPAN_PARAGRAPH是否相等,这里如果设置的是上述四个值,这里是不等的,所以不会进入该判断。

       设置了count,第一次count为0,设置了spans数组与data,第一次设置的值是在构造函数中初始化的值。

       因为count为0,因此for循环也不会进入

       之后判断了mSpanCount + 1 >= mSpans.length,这里前面为1,后面为0,因此会进入if判断,首先申请了一个3个长度的newtags数组,一个9个长度的int数组, 之后进行了两次数据拷贝,将已有的span拷贝到新申请的数组中,将其他参数拷贝到新的int数组中。

       之后将改成设置的span设置到mSpans数组中,将其他的参数设置到mSpanData,三个参数是连续设置的。

       最后调用了sendSpanAdded,代码如下:

private void sendSpanAdded(Object what, int start, int end) {
    SpanWatcher[] recip = getSpans(start, end, SpanWatcher.class);
    int n = recip.length;

    for (int i = 0; i < n; i++) {
        recip[i].onSpanAdded((Spannable) this, what, start, end);
    }
}

       这个调用了getSpans,返回了一个SpanWatcher数组,SpanWatcher是一个接口,MultiTapKeyListener, TextKeyListener实现了该类,因此当调用了TextKeyListener或者MultiTapKeyListener会对相应的span进行处理。

总结

       这里只是大致的解析了SpannableString,他还需要结合TextView进行分析,看看在界面绘制的时候是怎样解析显示的。后续有时间会陆续进行解析的。

       最后附一个链接,在我解析span的时候,解析了几个感觉太多,就搜索一下是否已经有人解析过,因此这个这里加上跳转链接,如果有版权或者不让导航,请告知,我好删除。传送门

你可能感兴趣的:(android,spannable)