Android 千变万化 TextView:神奇的 SpannableString

之前写过一篇SpannableString的文章,最近搬出来统一放在上。

前言

TextView 可以说是 Android 中最简单、最常见的文字控件了,几乎每个页面都有 TextView 的身影,绝大多数情况我们用 TextView 只是单纯地显示一个文本,但是 TextView 的功能远远不止如此哦,简单的 TextView 也能千变万化显示出各种效果,这一切都要归功于 SpannableString。

TextView 和 SpannableString 一起使用具体有哪些神奇的地方呢?本场 Chat 将全面地介绍 SpannableString 的用法,让你的 TextView 不再简单。

SpannableString

在 Android 中,常规的字符串类就是 String 或者 Charsequence,String 用的最多,有些人可能对 Charsequence 都有点陌生,EditText 的 getText() 返回的就是 Charsequence 对象。但是今天我们要介绍的 SpannableString 就是另一种更强大的字符串类。

Spannable 是什么意思?英语词典上还真不太好查,我自己的理解的意思是:可测量、可塑造的,所以 SpannableString 就是一种可测量可塑造的字符串。

1)默认 TextView 样式

默认 TextView 样式我们再熟悉不过了,看下截图,没啥好说的。

enter image description here

2)自定义字体

SpannableString 可以给 TextView 设置自定义字体样式,并且可以指定某几个字,其实 SpannableString 几乎所有的属性可可以指定到具体某几个字。

SpannableString ss = new SpannableString(txCustomTypeface.getText());
ss.setSpan(new TypefaceSpan("sans-serif"), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txCustomTypeface.setText(ss);

这里用到了一个新的类:TypefaceSpan,它就是用来设置字体样式的,参数有 5 个可选值:default、default-bold、monospace、serif、sans-serif。后面的 2 和 4 是需要生效的起始位置和结束位置。

enter image description here

在这个例子中,我们把 2 - 4 的文字设置成了 sans-serif 样式,但是竟然看不出任何差别。不过也不必奇怪,这些字体样式之间的差异确实非常小,根据一篇专业的字体研究报告称,sans 字体适合正文内容文字,能长时间集中视觉注意力,而 sans-serif 适合标题文字,能快速抓住注意力,但不适宜长时间阅读。总之,这之间的差别是比较专业的,在这个例子中确实看不出多大区别。

3)绝对字体和相对字体

SpannableString 可以动态地改变字体大小,并且支持绝对大小和相对大小两种模式。

绝对大小
SpannableString ss = new SpannableString(txAbsoluteSize.getText());
ss.setSpan(new AbsoluteSizeSpan(12, true), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txAbsoluteSize.setText(ss);

enter image description here

图中可以看到中间两个字变小了,AbsoluteSizeSpan 就是构建绝对大小的类,它有两个参数,第一个表示字体大小,第二个表示是否使用 DIP,false 的话单位就是 px,true 的话单位就是 dp。

相对大小
SpannableString ss = new SpannableString(txRelativeSize.getText());
ss.setSpan(new RelativeSizeSpan(1.5f), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txRelativeSize.setText(ss);

enter image description here

相对字体大小就简单一些了,只需要传入一个字体相对大小,比如我们传入了 1.5,中间两个字就变成了原始字体的 1.5 倍大。

4)前景色和背景色

其实对于 TextView 来说,前景色就是 textColor,背景色就是 background。你可能会觉得那为什么要用 SpannableString 来做呢,直接用 textColor 和 background 不就可以了吗?但是 textColor 和 background 只能对 textView 整体生效,而 SpannableString 可以动态给不同位置的文字设置不同颜色。

前景色
SpannableString ss = new SpannableString(txForegroundColor.getText());
ss.setSpan(new ForegroundColorSpan(Color.BLUE), 0, txForegroundColor.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txForegroundColor.setText(ss);

enter image description here
背景色
SpannableString ss = new SpannableString(txBackgroundColor.getText());
ss.setSpan(new BackgroundColorSpan(Color.LTGRAY), 0, 
    txBackgroundColor.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txBackgroundColor.setText(ss);

enter image description here

5)字体的加粗和倾斜

这里和大多数编辑器一样,支持三种:粗体、斜体、粗斜体

对应的常量是:Typeface.BOLD、Typeface.ITALIC、Typeface.BOLD_ITALIC

SpannableString ss = new SpannableString(txBord.getText());
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, txBord.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txBord.setText(ss);

enter image description here

6)删除线和下划线

删除线和下划线是两种常用文本标记符号,SpannableString 当然也是支持的。设置删除线和下划线很简单,只要指定起始位置和结束位置即可,下面直接看代码和效果图吧。

删除线

删除线用到的类是 StrikethroughSpan,没有参数。

SpannableString ss = new SpannableString(txDeleteLine.getText());
ss.setSpan(new StrikethroughSpan(), 0, txDeleteLine.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txDeleteLine.setText(ss);

enter image description here
下划线

下划线用到的类是 UnderlineSpan,没有参数。

SpannableString ss = new SpannableString(txUnderLine.getText());
ss.setSpan(new UnderlineSpan(), 0, txUnderLine.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txUnderLine.setText(ss);

enter image description here

7)文字的上标和下标

这个在实际开发中不常用,但是却很重要,因为万一遇到这种需求要自己实现的话还挺麻烦的。SpannableString 实现起来就很简单了。

SpannableString ss = new SpannableString(txSubSuperScript.getText());
ss.setSpan(new SuperscriptSpan(), 2, 3, SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new SubscriptSpan(), 5, 6, SPAN_EXCLUSIVE_EXCLUSIVE);
txSubSuperScript.setText(ss);

Android 千变万化 TextView:神奇的 SpannableString_第1张图片
enter image description here

8)6 种超链接形式

我记得我实习那会遇到一个需求要实现一个 TextView 中超链接的功能,那时候我还不知道 SpannableString,想了各种办法,头都大了。

SpannableString 支持 6 中超链接形式,分别是: 电话超链接、邮件超链接、网址超链接、短信超链接、彩信超链接、地图超链接。

a.电话超链接

这里又涉及到了一个新的类:URLSpan,实际上6种超链接都是使用 URLSpan 构建的,只是构造函数传入的链接格式不一样, 电话超链接传入的是 tel: 开头,后面接要拨打的电话号码,点击后就会自动跳转拨打电话。

SpannableString ss = new SpannableString(txTelUrl.getText());
ss.setSpan(new URLSpan("tel:02512345678"), 0, txTelUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txTelUrl.setText(ss);
txTelUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
b.邮件超链接

邮件超链接是以 mailto: 开头,后面接邮箱地址。点击后就会自动跳转邮件 app。

SpannableString ss = new SpannableString(txMailUrl.getText());
ss.setSpan(new URLSpan("mailto:[email protected]"), 0, txMailUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txMailUrl.setText(ss);
txMailUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here

如果你的手机里存在多个邮件 app,需要选择一个。

Android 千变万化 TextView:神奇的 SpannableString_第2张图片
enter image description here
c.网址超链接

网址超链接是以 http:// 或 https:// 开头,后面接网址,点击后跳转浏览器 app,同样如果有多个浏览器,需要作出选择。

SpannableString ss = new SpannableString(txWebUrl.getText());
ss.setSpan(new URLSpan("http://www.baidu.com"), 0, txWebUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txWebUrl.setText(ss);
txWebUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
d.短信超链接

短信超链接是以 sms: 开头,后面接手机号码,点击后跳转系统短信 app。

SpannableString ss = new SpannableString(txSmsUrl.getText());
ss.setSpan(new URLSpan("sms:02512345678"), 0, txSmsUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txSmsUrl.setText(ss);
txSmsUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
e.彩信超链接

彩信超链接是以 mms: 开头,后面接手机号码,点击永阳跳转系统短信 app。

SpannableString ss = new SpannableString(txMmsUrl.getText());
ss.setSpan(new URLSpan("mms:02512345678"), 0, txMmsUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txMmsUrl.setText(ss);
txMmsUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
f.地图超链接

地图超链接以 geo: 开头,后面接经纬度,点击后跳转地图 app。

SpannableString ss = new SpannableString(txGeoUrl.getText());
ss.setSpan(new URLSpan("geo:30.123456,-50.024456"), 0, 
    txGeoUrl.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txGeoUrl.setText(ss);
txGeoUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here

如果你的手机有多个地图 app,需要选择一个默认 app。

Android 千变万化 TextView:神奇的 SpannableString_第3张图片
enter image description here

9)添加项目符号

关于这一点,客观地说用处不大,SpannableString 虽然支持设置项目符号,但是实际开发中基本不会用,如果是页面中的栏位,我们肯定会用小 icon 实现项目符号,如果是 H5,那就是 HTML 的标签实现。

BulletSpan 类用于构建项目符号,第一个参数是项目符号所占的宽度,第二个参数是项目符号的颜色。

SpannableString ss = new SpannableString(txBullte.getText());
ss16.setSpan(new BulletSpan(20, Color.RED), 0, txBullte.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txBullte.setText(ss);

enter image description here

10)文字的横向和纵向拉伸

一般我们要改变字体大小,都是设置 textSize 属性,这个属性是文字整体等比例放大缩小,那如果我只想文字横向拉伸呢?这时候就要用到 SpannableString 了。

SpannableString ss = new SpannableString(txScaleX.getText());
ss.setSpan(new ScaleXSpan(2.5f), 0, txScaleX.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txScaleX.setText(ss);

enter image description here

ScaleXSpan 类用于指定横向拉伸的比例,我们传 2.5 表示横向拉伸为原来的 2.5 倍。

有了横向拉伸,自然我们会想纵向拉伸,不好意思,不支持。因为纵向的高度就得用 textSize 设置。

11)ColorStateList

这个东西我很少发现有人用,可能是因为不知道有这个类,也可能是因为这个用起来太麻烦。但不代表这个东西没用。

大家有没有遇到过这样的场景,一个 Button,默认灰色背景,黑色文字,按下后,背景要变成黑色,这个需求很常见,但是你有可能遇到这样的场景。

Android 千变万化 TextView:神奇的 SpannableString_第4张图片
enter image description here

本来文字就是黑色,按下后背景变成黑色,文字就看不见了,背景颜色和文字颜色的对比度太低了甚至为 0,导致文字不可见。

我们希望正常状态下背景灰色,文字黑色,按下状态背景变成黑色,文字变成白色。这时候就要用到 ColorStateList。

首先像以前一样定义一个 drawable,button_text.xml




    
    


然后解析 xml,构建 ColorStateList 并设置给 textView,效果就实现了。

ColorStateList csl = null;
try {
    =XmlResourceParser xrp = getResources().getXml(R.drawable.button_text);
    csl = ColorStateList.createFromXml(getResources(), xrp);
} catch (XmlPullParserException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
btn.setTextColor(csl);

Android 千变万化 TextView:神奇的 SpannableString_第5张图片
enter image description here

实战:表情文字

下面我们来做一个稍有难度的小项目:表情文字。 其效果就和常规的聊天软件一样,可以混合输入表情和文字,并且可以显示在聊天记录中。

Android 千变万化 TextView:神奇的 SpannableString_第6张图片
enter image description here

看上去效果还不错,表情和文字稍微有点不对齐(偏下),还可以再优化下,后面代码分析也会说到。文字和表情可以混排,输入框中输入的表情和聊天列表中显示一致,基本功能都实现了。下面就来看下是怎么实现的吧。

1)分析

整个过程可以分成两步,第一步是让输入框 EditText 可以输入表情,第二步是把输入框输入的表情显示到 TextView 上。

2)准备表情资源

我在网上下载了一批常用的表情图片,放在 drawable - xxhdpi 目录下:

Android 千变万化 TextView:神奇的 SpannableString_第7张图片
enter image description here

3)给表情编码

我们在 assets 目录下新建一个文件 emotion.xml,我们把每一个表情定义为一个 emotion,有 code 和 name 两个属性,name 就是表情图片的文件名。



    
        
        f001
    
    
        
        f002
    
    
        
        f003
    
    
        
        f004
    
    
        
        f005
    
    
        
        f006
    
    
        
        f007
    
    
        
        f008
    
    
        
        f009
    
    
        
        f010
    
    
        
        f011
    
    
        
        f012
    
    
        
        f013
    
    
        
        f014
    
    
        
        f015
    
    
        
        f016
    
    
        
        f017
    
    
        
        f018
    
    
        
        f019
    
    
        
        f020
    
    
        
        f021
    
    
        
        f022
    
    
        
        f023
    
    
        
        f024
    


4)解析 emotion.xml

xml 只是配置,最终肯定要解析成 java bean,下面是我的解析过程。

当然你也可以用 json 编码 emotion,然后解析 json,可能会比解析 xml 要简单些

public static List getEmotions(InputStream inputStream) {
    XmlPullParser parser = Xml.newPullParser();
    int eventType = 0;
    List emotions = null;
    Emotion emotion = null;
    try {
        parser.setInput(inputStream, "UTF-8");
        eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {

            switch (eventType) {
            case XmlPullParser.START_DOCUMENT:

                emotions = new ArrayList();
                break;
            case XmlPullParser.START_TAG:
                if ("emotion".equals(parser.getName())) {
                    emotion = new Emotion();

                } else if ("code".equals(parser.getName())) {
                    emotion.setCode(parser.nextText());
                } else if ("name".equals(parser.getName())) {
                    emotion.setName(parser.nextText());
                }
                break;
            case XmlPullParser.END_TAG:
                if ("emotion".equals(parser.getName())) {
                    emotions.add(emotion);
                    emotion = null;
                }
                break;
            default:
                break;
            }
            eventType = parser.next();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return emotions;
}

5)显示表情

拿到了表情列表,显示出来就简单了,我们随便用 GridView 或者 RecyclerView 都可以,太基础了,这部分代码就不放出来了,直接看下效果图吧。

Android 千变万化 TextView:神奇的 SpannableString_第8张图片
enter image description here

6)输入表情

哎,关键的地方来了,怎么把表情输入到 EditText 中呢?

我们这篇文章讲的是 SpannableString,那当然是用 SpannableString 做。

SpannableString 除了可以像前面那样把文字变大变小变长变色,还可以把一部分文字变成图片,承载图片的是 Drawable 对象,而实现这个效果的就是 ImageSpan。

看下基本使用方法

SpannableString ss = new SpannableString(str);
ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM);
ss.setSpan(span, 0, str.length(), SPAN_EXCLUSIVE_EXCLUSIVE);

ImageSpan 的构造函数要传 2 个参数,drawable 对象和对齐方式,这里的对齐方式就是表情和文字的对齐方式,只有两个选项:

ALIGN_ BASELINE 和 ALIGN_ BOTTOM,我这里选择的是 ALIGN_BOTTOM,所以表情相对文字会偏下。

这样设置后,字符串 str 就和 drawable 对象对应上了,在显示时会显示 drawable,但是调用 editText.getText() 得到的还是字符串。

弄懂了这个原理,再看下面代码就简单多了。

@Override
public void onItemClick(AdapterView p, View v, int position, long id) {
    Emotion emotion = emotions.get(position);
    int cursor = etInput.getSelectionStart();
    Field f;
    try {
        f = (Field) R.drawable.class.getDeclaredField(emotion.getName());
        int j = f.getInt(R.drawable.class);
        Drawable d = getResources().getDrawable(j);
        int textSize = (int)etInput.getTextSize();
        d.setBounds(0, 0, textSize, textSize);

        String str = null;
        int pos = position + 1;
        if (pos < 10) {
            str = "f00" + pos;
        } else if (pos < 100) {
            str = "f0" + pos;
        } else {
            str = "f" + pos;
        }
        SpannableString ss = new SpannableString(str);
        ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM);
        ss.setSpan(span, 0, str.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        etInput.getText().insert(cursor, ss);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上述代码可简单分析成以下步骤:
(1)根据点击位置,获取到该位置的 Emotion 对象。
(2)根据 emotion 的 name,通过反射的方式获取到 Drawable 对象。
(3)根据 EditText 的 textSize 设置 drawable 的大小,为了看上去表情和文字是协调的,我直接把 drawable 的宽高设置成了textSize。
(4)构建 ImageSpan 和 SpannableString,把 drawable 和字符串 str 对应起来。
(5)把 SpannableString 插入到 EditText 当前光标位置。

这样解释是不是太简单了,可是代码确实很简单啊。至此,我们算是实现了第一步:在 EditText 中输入表情,接下来就要实现第二步,把输入的表情显示在聊天记录中。

7)把输入的表情显示在聊天列表

我们既然已经把表情输入到 EditText 了,显示到 TextView 还不简单,直接把 SpannableString 设置给 TextView 不就行了吗?

在 demo 中是可以,但是在实际项目中不行。实际项目中输入的内容是要转成 String 传输的,再发给客户端,客户端接收到消息后再解析显示。所以这就需要再执行一次构建 SpannableString 的操作,具体代码如下:

(1)首先获取 EditText 输入的内容,然后经过一个 getExpressionString 方法转成 SpannableString,然后添加到 adapter 中刷新聊天列表,最后清空输入框。

public void onSendClick() {
    String receiveStr = etInput.getText().toString();
    SpannableString ss= getExpressionString(this, receiveStr, textSize);
    messages.add(ss);
    adapter.notifyDataSetChanged();
    lvMsg.setSelection(messages.size() - 1);
    etInput.setText(null);
}

(2)那么重点就是 getExpressionString 方法了,这个方法构建一个 SpannableString 和一个正则匹配模式,接着又调用了 dealExpression 方法。

public static final String PATTEN_STR = "f0[0-9]{2}|f10[0-7]";

public SpannableString getExpressionString(Context context, String str, 
        int textSize) {
    SpannableString ss = new SpannableString(str);
    Pattern sinaPatten = Pattern.compile(PATTEN_STR, Pattern.CASE_INSENSITIVE);
    try {
        dealExpression(context, ss, textSize, sinaPatten, 0);
    } catch (Exception e) {
        Log.e("dealExpression", e.getMessage());
    }
    return ss;
}

(3)真正的重点来了,这个方法中利用正则匹配模式,找到输入内容中每一条符合正则的子字符串,也就是表情编码的字符串,然后像之前那样通过反射获取 Drawable,构建 SpannableString 把 Drawable 和 String 对应起来。

(此部分代码和之前是一样的)

public void dealExpression(Context context, SpannableString ss, 
        int textSize, Pattern patten, int start) throws Exception {
    Matcher matcher = patten.matcher(ss);
    while (matcher.find()) {
        String key = matcher.group();
        if (matcher.start() < start) {
            continue;
        }
        Field field = R.drawable.class.getDeclaredField(key);
        int resId = field.getInt(R.drawable.class);
        if (resId != 0) {
            Drawable d = context.getResources().getDrawable(resId);
            d.setBounds(0, 0, textSize, textSize);
            ImageSpan imageSpan = new ImageSpan(d);
            int end = matcher.start() + key.length();
            ss.setSpan(imageSpan, matcher.start(), end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            if (end < ss.length()) {
                dealExpression(context, ss, textSize, patten, end);
            }
            break;
        }
    }
}

看到这你明白了吗?整个过程就是操作 SpannableString 的过程,SpannableString 内部通过 ImageSpan 把字符串和 Drawable 对应起来,在显示的时候表现为 Drawable,在 getText 时表现为普通 String。

就是这么简单,以前可能觉得表情文字是很神奇的存在,现在是不是觉得就是纸老虎。

大工告成!至此,整个实现的逻辑就讲完了,但是我的工程中远不止这些,还有很多边缘性的功能,但核心的东西都讲了。

最后,我把完整的工程代码放出来,需要的朋友下载吧。
https://gitee.com/alexandor/EmotionText

好了,以上就是本期 Chat 的全部内容,感谢大家的支持,如有错误或不当之处还请指出。

你可能感兴趣的:(Android 千变万化 TextView:神奇的 SpannableString)