IT技术互动交流平台

AndroidSpannableString浅析

来源:IT165收集  发布日期:2016-05-16 20:47:49

引言

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

需求样例

比如我们需要实现如下图所示的功能,将文本:#重磅消息#近日谷歌放出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进行分析,查看链接,现在还未实现

方案4

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

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

    SpannableStringBuilder sb = new SpannableStringBuilder(originText);
    sb.append('
').append('
').append('
');
    getEffect1Span(sb);
    sb.append('
').append('
').append('
');
    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('
');
    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('近日
谷歌放出Android 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包下查看,这么多,整个人都不好了。

这里写图片描述
因此先就针对上面的做了部分样例,之后会专门实现一下每个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 != '
')
                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 != '
')
                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的时候,解析了几个感觉太多,就搜索一下是否已经有人解析过,因此这个这里加上跳转链接,如果有版权或者不让导航,请告知,我好删除。传送门

延伸阅读:

Tag标签: AndroidSpannableString浅析  
  • 专题推荐

About IT165 - 广告服务 - 隐私声明 - 版权申明 - 免责条款 - 网站地图 - 网友投稿 - 联系方式
本站内容来自于互联网,仅供用于网络技术学习,学习中请遵循相关法律法规