Android利用SpannableString实现格式化微博内容
前言
在android开发中,有许多信息展示需要通过textview来展现,如果只是普通的信息展现,使用textview settext(charsequence str)设置即可,但是当在textview里的这段内容需要截取某一部分字段,可以被点击以及响应响应的操作,这时候就需要用到spannablestring了,spannablestring 配合 textview 可以轻松实现对特定的文本做特定处理,例如可以修改文字颜色、背景色、将文字替换为图片实现,点击效果等。
首先看看最终实现的效果图:
第一个卡片内的微博是原始文本信息,第二个卡片内的微博是第一个格式化后的文本内容,将微博内的”话题”、”表情”、”网页链接”、以及”@用户”都进行了处理,并可以点击,使其和官方微博展示的样式保持一致。
要实现的效果:
- 将话题进行变色并且可以点击提示对应的话题文本内容
- 将图片表情替换掉对应的表情关键字显示
- 将链接地址替换成一个链接的图片和”网页链接”四个字显示
- 将@的用户进行变色并且可以点击提示对应的话题文本内容
需要:
- 使用正则表达式提取文本内对应的”话题”、”表情”、”网页链接”、以及”@用户”内容
- 使用 spannablestring 格式化提取到的文本
- 给格式化的部分添加点击事件
定义正则表达式
首先定义”话题”、”表情”、”网页链接”、以及”@用户”对应的正则表达式和对应的 pattern。scheme 下文会提到具体的用处的。
public class weibopattern { // #话题# public static final string regex_topic = "#[\\p{print}\\p{incjkunifiedideographs}&&[^#]]+#"; // [表情] public static final string regex_emotion = "\\[(\\s+?)\\]"; // url public static final string regex_url = "http://[a-za-z0-9+&@#/%?=~_\\\\-|!:,\\\\.;]*[a-za-z0-9+&@#/%=~_|]"; // @人 public static final string regex_at = "@[\\w\\p{incjkunifiedideographs}-]{1,26}"; public static final pattern pattern_topic = pattern.compile(regex_topic); public static final pattern pattern_emotion = pattern.compile(regex_emotion); public static final pattern pattern_url = pattern.compile(regex_url); public static final pattern pattern_at = pattern.compile(regex_at); public static final string scheme_topic = "topic:"; public static final string scheme_url = "url:"; public static final string scheme_at = "at:"; }
提取匹配部分并使用 spannablestring 格式化
我将此过程写到一个方法内了,下面直接上代码,代码中有详细的注释解释:
/** * 格式化微博文本 * * @param context 上下文 * @param source 源文本 * @param textview 目标 textview * @return spannablestringbuilder */ public static spannablestringbuilder formatweibocontent(context context, string source, textview textview) { // 获取到 textview 的文字大小,后面的 imagespan 需要用到该值 int textsize = (int) textview.gettextsize(); // 若要部分 spannablestring 可点击,需要如下设置 textview.setmovementmethod(linkmovementmethod.getinstance()); // 将要格式化的 string 构建成一个 spannablestringbuilder spannablestringbuilder value = new spannablestringbuilder(source); // 使用正则匹配话题 linkify.addlinks(value, weibopattern.pattern_topic, weibopattern.scheme_topic); // 使用正则匹配链接 linkify.addlinks(value, weibopattern.pattern_url, weibopattern.scheme_url); // 使用正则匹配@用户 linkify.addlinks(value, weibopattern.pattern_at, weibopattern.scheme_at); // 自定义的匹配部分的点击效果 myclickablespan clickspan; // 获取上面到所有 addlinks 后的匹配部分(这里一个匹配项被封装成了一个 urlspan 对象) urlspan[] urlspans = value.getspans(0, value.length(), urlspan.class); // 遍历所有的 urlspan for (final urlspan urlspan : urlspans) { // 点击匹配部分效果 clickspan = new myclickablespan() { @override public void onclick(view view) { toastutils.makeshort(urlspan.geturl()); } }; // 话题 if (urlspan.geturl().startswith(weibopattern.scheme_topic)) { int start = value.getspanstart(urlspan); int end = value.getspanend(urlspan); value.removespan(urlspan); // 格式化话题部分文本 value.setspan(clickspan, start, end, spanned.span_exclusive_exclusive); } // @用户 if (urlspan.geturl().startswith(weibopattern.scheme_at)) { int start = value.getspanstart(urlspan); int end = value.getspanend(urlspan); value.removespan(urlspan); // 格式化@用户部分文本 value.setspan(clickspan, start, end, spanned.span_exclusive_exclusive); } // 链接 if (urlspan.geturl().startswith(weibopattern.scheme_url)) { int start = value.getspanstart(urlspan); int end = value.getspanend(urlspan); value.removespan(urlspan); spannablestringbuilder urlspannablestring = geturltextspannablestring(context, urlspan.geturl(), textsize); value.replace(start, end, urlspannablestring); // 格式化链接部分文本 value.setspan(clickspan, start, start + urlspannablestring.length(), spanned.span_exclusive_exclusive); } } // 表情需要单独格式化 matcher emotionmatcher = weibopattern.pattern_emotion.matcher(value); while (emotionmatcher.find()) { string emotion = emotionmatcher.group(); int start = emotionmatcher.start(); int end = emotionmatcher.end(); int resid = emotionutils.getimagebyname(emotion); if (resid != -1) { // 表情匹配 l.e("find emotion: " + emotion); drawable drawable = context.getresources().getdrawable(resid); drawable.setbounds(0, 0, (int) (textsize * 1.3), (int) (textsize * 1.3)); // 自定义的 verticalimagespan ,可解决默认的 imagespan 不垂直居中的问题 verticalimagespan imagespan = new verticalimagespan(drawable); value.setspan(imagespan, start, end, spannable.span_exclusive_exclusive); } } return value; }
private static spannablestringbuilder geturltextspannablestring(context context, string source, int size) { spannablestringbuilder builder = new spannablestringbuilder(source); string prefix = " "; builder.replace(0, prefix.length(), prefix); drawable drawable = context.getresources().getdrawable(r.drawable.ic_status_link); drawable.setbounds(0, 0, size, size); builder.setspan(new verticalimagespan(drawable), prefix.length(), source.length(), spannable.span_exclusive_exclusive); builder.append(" 网页链接"); return builder; }
geturltextspannablestring()
:方法是用来返回一个图标+”网页链接” spannablestring,用于替换链接文本
上面将”话题”、”表情”、”网页链接”都用了addlinks方法来标记的,然后统一处理。表情则是单独处理的。
表情则使用如下方法事先做好映射:
public class emotionutils { public static linkedhashmap<string, integer> smap; static { smap = new linkedhashmap<>(); smap.put("[doge]", r.drawable.d_doge); smap.put("[污]", r.drawable.d_wu); } public static int getimagebyname(string name) { integer integer = smap.get(name); return integer == null ? -1 : integer; } }
还有刚才说到的自定义 myclickablespan 修改默认的样式:
public class myclickablespan extends clickablespan { @override public void onclick(view view) { } @override public void updatedrawstate(textpaint ds) { super.updatedrawstate(ds); ds.setcolor(0xff03a9f4); ds.setunderlinetext(false); } }
另外,由于默认的 imagespan 在 textview 有使用android:linespacingextra属性时,不会垂直居中,所以使用到了网上的一个继承自 imagespan 的 verticalimagespan 可以做到保持图片在 textview 内保持垂直居中:
public class verticalimagespan extends imagespan { public verticalimagespan(drawable drawable) { super(drawable); } /** * update the text line height */ @override public int getsize(paint paint, charsequence text, int start, int end, paint.fontmetricsint fontmetricsint) { drawable drawable = getdrawable(); rect rect = drawable.getbounds(); if (fontmetricsint != null) { paint.fontmetricsint fmpaint = paint.getfontmetricsint(); int fontheight = fmpaint.descent - fmpaint.ascent; int drheight = rect.bottom - rect.top; int centery = fmpaint.ascent + fontheight / 2; fontmetricsint.ascent = centery - drheight / 2; fontmetricsint.top = fontmetricsint.ascent; fontmetricsint.bottom = centery + drheight / 2; fontmetricsint.descent = fontmetricsint.bottom; } return rect.right; } /** * see detail message in android.text.textline * * @param canvas the canvas, can be null if not rendering * @param text the text to be draw * @param start the text start position * @param end the text end position * @param x the edge of the replacement closest to the leading margin * @param top the top of the line * @param y the baseline * @param bottom the bottom of the line * @param paint the work paint */ @override public void draw(canvas canvas, charsequence text, int start, int end, float x, int top, int y, int bottom, paint paint) { drawable drawable = getdrawable(); canvas.save(); paint.fontmetricsint fmpaint = paint.getfontmetricsint(); int fontheight = fmpaint.descent - fmpaint.ascent; int centery = y + fmpaint.descent - fontheight / 2; int transy = centery - (drawable.getbounds().bottom - drawable.getbounds().top) / 2; canvas.translate(x, transy); drawable.draw(canvas); canvas.restore(); } }
然后直接调用该方法格式化:
mtextview.settext(formatweibocontent(this,mtextview.gettext().tostring(),mtextview))
最终的效果图和文章开头效果一样了,并且可以点击,这里展示了点击”网页链接”时弹出的 toast 提示:
总结
本文仅介绍了 spannablestring 常用的一些场景,例如修改特定文本的颜色,替换特定文本,特定文本的点击事件,但是 spannablestring 的强大远不止如此。spannablestring 的更多用法可阅读官方文档。好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。