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进行分析,查看链接,现在还未实现

方案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包下查看,这么多,整个人都不好了。

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

时间: 2024-07-28 19:59:31

Android SpannableString浅析的相关文章

Android Cursor浅析

1. 本文目的 Android ContentProvider提供了进程间数据交换的一种机制.而数据库的查询就是这种机制的应用.那么app通过Uri查询数据库而得到的Cursor究竟是个什么东西?为何可以为我们提供另一个进程的数据?本文以getContentResolver().query(--)函数为起点,全面分析Cursor家族关系类图,理清Cursor跨进程通信的机制. 1.1 客户端的Cursor对象 假设B进程中有一个ContentProvider,A进程通过Uri查询这个Conten

Android Annotations浅析

这阵子遇到了好多事情,挺久没来更新博文了,这两天在学这个开源框架Android Annotations,用起来感觉挺方便的, 相信用过Spring注解的孩子理解起来应该比较容易!就是配置起来比较吃力. 关于AndroidAnnotaions的介绍,网上已经很多了,我这里不再累赘. 1.AndroidAnnotations官网:http://androidannotations.org/(也许你需要翻墙) 2.eclipse中使用androidannotations的配置方法说明:https://

Android apps浅析01-Amazed:一个简单但令人上瘾的加速度为基础的大理石指导游戏。

Android apps浅析01-Amazed:一个简单但令人上瘾的加速度为基础的大理石指导游戏. 这个例子中只有4个类,一个绘制大理石类Marble,一个绘制迷宫类Maze,一个Amazed视图类,一个Amazed活动类 1. 绘制大理石类Marble通过Canvas和Paint绘制,同时提供移动x轴和y轴坐标的方法,每个大理石都有一个状态值:活的/死的 /* * Copyright (C) 2008 Jason Tomlinson. * * Licensed under the Apache

Android apps浅析02-Android IM:一个类似手机QQ的即时通讯开源实现

Android apps浅析02-Android IM:一个类似手机QQ的即时通讯开源实现 这是Android上的一个简单的IM应用程序运行时,应用程序发出HTTP请求到服务器,在PHP和MySQL,验证,注册和得到其他朋友的状态和数据来实现,那么它与其他设备的其他应用程序通过通信套接字接口. 1. 数据库只要2个表:朋友表和用户表: CREATE TABLE `friends` ( `Id` int(10) unsigned NOT NULL auto_increment, `provider

(Android系统)android property浅析

android property,相信各位android平台的开发人员用到的不会少,但是property的具体机制大家可能知道的不多,这里利用空闲时间大致了解了一些,特此分享跟大家,如有谬误,欢迎指正 android 1号进程进程init进程在开机的时候就会调用property_init函数,至于init是怎么起来的,这里不是重点,所以暂时先不介绍,property_init的具体flow如下: system/core/init/init.c void property_init(void) {

Android - SpannableString或SpannableStringBuilder以及string.xml文件中的整型和string型代替

背景介绍 在开发应用过程中经常会遇到显示一些不同的字体风格的信息犹如默认的LockScreen上面的时间和充电信息.对于类似的情况,可能第一反应就是用不同的多个TextView来实现,对于每个TextView设置不同的字体风格以满足需求. 这里推荐的做法是使用android.text.*;和 android.text.style.*;下面的组件来实现RichText:也即在同一个TextView中设置不同的字体风格.对于某些应用,比如文本编辑,记事本,彩信,短信等地方,还必须使用这些组件才能达到

Android SpannableString 基本用法

以下介绍SpannableString 对文字的一些特别处理:比如字体,颜色,下划线,链接和点击事件. 先看效果: 布局代码就不贴了,很简单就是3个TextView.现在看MainActivity中的代码实现: private TextView mTvContent1; private TextView mTvContent2; private TextView mTvContent3; private String contentStr1 = "点击事件,下划线,字体颜色"; pri

Android动画浅析

前言 在应用的开发中,动画是必不可少的一部分,否则很多视觉效果会显得特别突兀.今天我们就来了解一下Android中的动画库. 动画的分类 Android平台为我们提供了两类动画,Tween(补间动画)动画和Frame(帧)动画.Tween动画是通过对场景中的对象不断进行图像变换(平移.缩放.旋转等)来产生动画效果的:Frame动画则是顺序播放事先做好的每帧图像,类似于快速的幻灯片一样. 补间动画 Tween动画是通过预先定义一个动画,这个动画指定了图形变换的类型(旋转.平移.缩放等).启动时间.

android framework浅析_转

Android系统从底向上一共分了4层,每一层都把底层实现封装,并暴露调用接口给上一层. 1. Linux内核(Linux Kernel) 1)Android运行在linux kernel 2.6之上,但是把linux内受GNU协议约束的部分做了取代,这样在Android的程序可以用于商业目的. 2)Linux 内核是硬件和软件层之间的抽象层. 3)系统总是需要操作系统的支持的,比如内存管理.进程管理.网络协议栈等 2. 中间件(即android 的C/C++框架) 1)中间件包括两部分:核心库