之前写过一篇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 样式我们再熟悉不过了,看下截图,没啥好说的。
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 是需要生效的起始位置和结束位置。
在这个例子中,我们把 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);
图中可以看到中间两个字变小了,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);
相对字体大小就简单一些了,只需要传入一个字体相对大小,比如我们传入了 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);
背景色
SpannableString ss = new SpannableString(txBackgroundColor.getText());
ss.setSpan(new BackgroundColorSpan(Color.LTGRAY), 0,
txBackgroundColor.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txBackgroundColor.setText(ss);
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);
6)删除线和下划线
删除线和下划线是两种常用文本标记符号,SpannableString 当然也是支持的。设置删除线和下划线很简单,只要指定起始位置和结束位置即可,下面直接看代码和效果图吧。
删除线
删除线用到的类是 StrikethroughSpan,没有参数。
SpannableString ss = new SpannableString(txDeleteLine.getText());
ss.setSpan(new StrikethroughSpan(), 0, txDeleteLine.getText().length(),
SPAN_EXCLUSIVE_EXCLUSIVE);
txDeleteLine.setText(ss);
下划线
下划线用到的类是 UnderlineSpan,没有参数。
SpannableString ss = new SpannableString(txUnderLine.getText());
ss.setSpan(new UnderlineSpan(), 0, txUnderLine.getText().length(),
SPAN_EXCLUSIVE_EXCLUSIVE);
txUnderLine.setText(ss);
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);
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());
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());
如果你的手机里存在多个邮件 app,需要选择一个。
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());
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());
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());
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());
如果你的手机有多个地图 app,需要选择一个默认 app。
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);
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);
ScaleXSpan 类用于指定横向拉伸的比例,我们传 2.5 表示横向拉伸为原来的 2.5 倍。
有了横向拉伸,自然我们会想纵向拉伸,不好意思,不支持。因为纵向的高度就得用 textSize 设置。
11)ColorStateList
这个东西我很少发现有人用,可能是因为不知道有这个类,也可能是因为这个用起来太麻烦。但不代表这个东西没用。
大家有没有遇到过这样的场景,一个 Button,默认灰色背景,黑色文字,按下后,背景要变成黑色,这个需求很常见,但是你有可能遇到这样的场景。
本来文字就是黑色,按下后背景变成黑色,文字就看不见了,背景颜色和文字颜色的对比度太低了甚至为 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);
实战:表情文字
下面我们来做一个稍有难度的小项目:表情文字。 其效果就和常规的聊天软件一样,可以混合输入表情和文字,并且可以显示在聊天记录中。
看上去效果还不错,表情和文字稍微有点不对齐(偏下),还可以再优化下,后面代码分析也会说到。文字和表情可以混排,输入框中输入的表情和聊天列表中显示一致,基本功能都实现了。下面就来看下是怎么实现的吧。
1)分析
整个过程可以分成两步,第一步是让输入框 EditText 可以输入表情,第二步是把输入框输入的表情显示到 TextView 上。
2)准备表情资源
我在网上下载了一批常用的表情图片,放在 drawable - xxhdpi 目录下:
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 都可以,太基础了,这部分代码就不放出来了,直接看下效果图吧。
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 的全部内容,感谢大家的支持,如有错误或不当之处还请指出。