Android中的TextView,本身就支持部分的Html格式标签。这其中包括常用的字体大小颜色设置,文本链接等。使用起来也比较方便,只需要使用Html类转换一下即可。比如:

textView.setText(Html.fromHtml(str));

然而,有一种场合,默认支持的标签可能不够用。比如,我们需要在textView中点击某种链接,返回到应用中的某个界面,而不仅仅是网络连接,如何实现?

经过几个小时对android中的Html类源代码的研究,找到了解决办法,并且测试通过。

先看Html类的源代码中有这样一段:

  1. /**
  2. * Is notified when HTML tags are encountered that the parser does
  3. * not know how to interpret.
  4. */
  5. public static interface TagHandler {
  6. /**
  7. * This method will be called whenn the HTML parser encounters
  8. * a tag that it does not know how to interpret.
  9. */
  10. public void handleTag(boolean opening, String tag,
  11. Editable output, XMLReader xmlReader);

这里定义了一个接口,接口用于什么呢?

再继续看代码,看到对Html的tag进行解析部分的代码:

  1. private void handleStartTag(String tag, Attributes attributes) {
  2. if (tag.equalsIgnoreCase("br")) {
  3. // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
  4. // so we can safely emite the linebreaks when we handle the close tag.
  5. } else if (tag.equalsIgnoreCase("p")) {
  6. handleP(mSpannableStringBuilder);
  7. } else if (tag.equalsIgnoreCase("div")) {
  8. handleP(mSpannableStringBuilder);
  9. } else if (tag.equalsIgnoreCase("em")) {
  10. start(mSpannableStringBuilder, new Bold());
  11. } else if (tag.equalsIgnoreCase("b")) {
  12. start(mSpannableStringBuilder, new Bold());
  13. } else if (tag.equalsIgnoreCase("strong")) {
  14. start(mSpannableStringBuilder, new Italic());
  15. } else if (tag.equalsIgnoreCase("cite")) {
  16. start(mSpannableStringBuilder, new Italic());
  17. } else if (tag.equalsIgnoreCase("dfn")) {
  18. start(mSpannableStringBuilder, new Italic());
  19. } else if (tag.equalsIgnoreCase("i")) {
  20. start(mSpannableStringBuilder, new Italic());
  21. } else if (tag.equalsIgnoreCase("big")) {
  22. start(mSpannableStringBuilder, new Big());
  23. } else if (tag.equalsIgnoreCase("small")) {
  24. start(mSpannableStringBuilder, new Small());
  25. } else if (tag.equalsIgnoreCase("font")) {
  26. startFont(mSpannableStringBuilder, attributes);
  27. } else if (tag.equalsIgnoreCase("blockquote")) {
  28. handleP(mSpannableStringBuilder);
  29. start(mSpannableStringBuilder, new Blockquote());
  30. } else if (tag.equalsIgnoreCase("tt")) {
  31. start(mSpannableStringBuilder, new Monospace());
  32. } else if (tag.equalsIgnoreCase("a")) {
  33. startA(mSpannableStringBuilder, attributes);
  34. } else if (tag.equalsIgnoreCase("u")) {
  35. start(mSpannableStringBuilder, new Underline());
  36. } else if (tag.equalsIgnoreCase("sup")) {
  37. start(mSpannableStringBuilder, new Super());
  38. } else if (tag.equalsIgnoreCase("sub")) {
  39. start(mSpannableStringBuilder, new Sub());
  40. } else if (tag.length() == 2 &&
  41. Character.toLowerCase(tag.charAt(0)) == 'h' &&
  42. tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
  43. handleP(mSpannableStringBuilder);
  44. start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
  45. } else if (tag.equalsIgnoreCase("img")) {
  46. startImg(mSpannableStringBuilder, attributes, mImageGetter);
  47. } else if (mTagHandler != null) {
  48. mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
  49. }
  50. }
  51. private void handleEndTag(String tag) {
  52. if (tag.equalsIgnoreCase("br")) {
  53. handleBr(mSpannableStringBuilder);
  54. } else if (tag.equalsIgnoreCase("p")) {
  55. handleP(mSpannableStringBuilder);
  56. } else if (tag.equalsIgnoreCase("div")) {
  57. handleP(mSpannableStringBuilder);
  58. } else if (tag.equalsIgnoreCase("em")) {
  59. end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
  60. } else if (tag.equalsIgnoreCase("b")) {
  61. end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
  62. } else if (tag.equalsIgnoreCase("strong")) {
  63. end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
  64. } else if (tag.equalsIgnoreCase("cite")) {
  65. end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
  66. } else if (tag.equalsIgnoreCase("dfn")) {
  67. end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
  68. } else if (tag.equalsIgnoreCase("i")) {
  69. end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
  70. } else if (tag.equalsIgnoreCase("big")) {
  71. end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
  72. } else if (tag.equalsIgnoreCase("small")) {
  73. end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
  74. } else if (tag.equalsIgnoreCase("font")) {
  75. endFont(mSpannableStringBuilder);
  76. } else if (tag.equalsIgnoreCase("blockquote")) {
  77. handleP(mSpannableStringBuilder);
  78. end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
  79. } else if (tag.equalsIgnoreCase("tt")) {
  80. end(mSpannableStringBuilder, Monospace.class,
  81. new TypefaceSpan("monospace"));
  82. } else if (tag.equalsIgnoreCase("a")) {
  83. endA(mSpannableStringBuilder);
  84. } else if (tag.equalsIgnoreCase("u")) {
  85. end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
  86. } else if (tag.equalsIgnoreCase("sup")) {
  87. end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
  88. } else if (tag.equalsIgnoreCase("sub")) {
  89. end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
  90. } else if (tag.length() == 2 &&
  91. Character.toLowerCase(tag.charAt(0)) == 'h' &&
  92. tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
  93. handleP(mSpannableStringBuilder);
  94. endHeader(mSpannableStringBuilder);
  95. } else if (mTagHandler != null) {
  96. mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
  97. }
  98. }

可以看到,如果不是默认的标签,会调用mTagHandler的handleTag方法。所以,我们可以实现此接口,来解析自己定义的标签类型。

再看一段我实现的对<game>标签进行解析的示例代码:

  1. public class GameTagHandler implements TagHandler {
  2. private int startIndex = 0;
  3. private int stopIndex = 0;
  4. @Override
  5. public void handleTag(boolean opening, String tag, Editable output,
  6. XMLReader xmlReader) {
  7. if (tag.toLowerCase().equals("game")) {
  8. if (opening) {
  9. startGame(tag, output, xmlReader);
  10. } else {
  11. endGame(tag, output, xmlReader);
  12. }
  13. }
  14. }
  15. public void startGame(String tag, Editable output, XMLReader xmlReader) {
  16. startIndex = output.length();
  17. }
  18. public void endGame(String tag, Editable output, XMLReader xmlReader) {
  19. stopIndex = output.length();
  20. output.setSpan(new GameSpan(), startIndex, stopIndex,
  21. Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
  22. }
  23. private class GameSpan extends ClickableSpan implements OnClickListener {
  24. @Override
  25. public void onClick(View v) {
  26. // 跳转某页面
  27. }
  28. }

上面这段代码,是对<game>…</game>的自定义标签进行解析。

具体调用方法:

textView.setText(Html.fromHtml(“点击<game>这里</game>跳转到游戏”,

null, new GameTagHandler()));

textView.setClickable(true);

textView.setMovementMethod(LinkMovementMethod.getInstance());

运行后,能够看到文本中的字符串“这里”带了超链接,点击链接后,GameSpan类的onClick()方法被调用。就可以在这个方法中进行跳转了。

看了一下第一种方式,直接使用SpannableString明显是不可行的,因为我们必须知道他的具体长度,那么只能够换一种方式实现了,相信有写过Html的大神们都知道其实Android有一个类叫Html,里面是支持我们Html格式的字符串转换为文本的,那么这时候思路就很清晰了,我们只需要接收Html格式的String,然后使用Html.fromHtml方法就可以将他转换为我们想要的多样式TextView!,马上动手试试。

3.3、代码

我们定义一个String假装他是服务器传递过来的数据进行显示看看结果是怎么样的,首先是来一个html格式的字符串

String htmlStr = "<font color='#0000FF' size='50px'>我是蓝色的文本</font><br><font color='#ff0000' size='40px'>我是红色的文本</font><br><font color='#000000' size='29px'>我是黑色的文本</font>";

 @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = new TextView(this);
mTextView.setText(Html.fromHtml(htmlStr));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

哈哈,这么简单的代码对于我们来说不就是分分钟就搞定吗,马上就来验证一下自己的成果 
 
我擦,颜色出来了,但是字体大小怎么完全没改变,你是不是在逗我,凭我多年写Html的Hello World语句来说我的Html文本肯定没有错!,立马找一下原因是为什么,让我们来看看Html.fromHtml都做了什么事情。


从源码里可以看到他会去定义一个SAX解析类Parser,然后传递到133行HtmlToSpannedConverter的构造方法里,并且调用这个类的conver()方法,那我们先看看这个类里面都做了什么 

简单过一下构造方法,知道他都有什么


如果有写过SAX解析的朋友现在肯定不会陌生,首先是去设置一个文档内容的处理器,进行XML的解析,里面就是一些头节点尾节点元素开头结束等等的XML相关处理,然后调用parser进行解析,然后会走回调方法,就列出我们比较关心的头尾方法


然后进行节点的处理



这时候眼睛比较凌厉的朋友已经发现我们最想要知道的代码了!他是如何去处理font这个标签的,让我们来看看这个方法startFont(mSpannableStringBuilder, attributes);


看到这里的时候我的心里是奔溃的。。尼玛这都什么跟什么,怎么就支持color这个标签,不支持size,还有这face是什么鬼,能支持face难道不能支持我传说中的大size么!!简直是在逗我!! 
在这里我们可以看出来他的实现方式其实很简单,首先是使用XML去解析每一个节点,然后使用SpannableStringBuilder去进行拼接。

到了这个时候我们只能够自己去定义实现font以及获取里面的属性了,要怎么做呢,我们可以看到其实他在这一大堆if else的判断里面已经把font这个标签给处理掉了,不会给我们继续处理(不要跟我说修改源码),这时候其实我们看一下if else的最后,他是会进行回调到一个叫TagHandler里面的方法的,那么我们只需要去实现这个接口就可以了,从上面可以看出来,他是一个抽象类,用于给我们扩展的

上面说到了font标签已经被处理掉了,不会再回调给我们,所以我们就需要自己去定义一个标签,当然了,后端给予我们得一样可以是font的,我们只是自己去进行替换,类似这样子

htmlStr = htmlStr.replaceAll("font", "bluefont");
mTextView.setText(Html.fromHtml(htmlStr, null, new HtmlTagHandler()));
  • 1
  • 2

首先是替换成一个源码里不会进行处理的标签

/**
* Created by blue.
*/
public class HtmlTagHandler implements Html.TagHandler {
private static final String TAG_BLUE_FONT = "bluefont"; private int startIndex = 0;
private int stopIndex = 0;
final HashMap<String, String> attributes = new HashMap<String, String>(); @Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
processAttributes(xmlReader); if(tag.equalsIgnoreCase(TAG_BLUE_FONT)){
if(opening){
startFont(tag, output, xmlReader);
}else{
endFont(tag, output, xmlReader);
}
}
} public void startFont(String tag, Editable output, XMLReader xmlReader) {
startIndex = output.length();
} public void endFont(String tag, Editable output, XMLReader xmlReader){
stopIndex = output.length(); String color = attributes.get("color");
String size = attributes.get("size");
size = size.split("px")[0]; if(!TextUtils.isEmpty(color) && !TextUtils.isEmpty(size)){
output.setSpan(new ForegroundColorSpan(Color.parseColor(color)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if(!TextUtils.isEmpty(size)){
output.setSpan(new AbsoluteSizeSpan(Integer.parseInt(size)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} private void processAttributes(final XMLReader xmlReader) {
try {
Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[])dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer)lengthField.get(atts); for(int i = 0; i < len; i++){
attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
}
}
catch (Exception e) { }
} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

使用startIndex和stopIndex来进行判断每一个标签头和尾的位置,对里面的文本进行相对应的样式处理。然后让我们来运行一下代码试试

 
恩!?为什么第一行的样式不起效果呢??其实这个只是SAX解析的一些小bug而已,具体原理的话稍后我再贴上来,解决的方案也很简单: 
1)在这一段Html格式的字符串前面再加上随意一个标签,例如<p>标签等等 
2)发送html格式的字符串过来的时候将<html><body>也就是一整个网页需要的信息传递过来,也可以解决这个问题

那么修复了这个小bug后让我们来看看我们的最终成功,一个字段显示多种不同样式的文本 

四、总结

上面的这些都是抛砖引玉,带来一些思路,我们可以自己进行扩展所有的Html标签,Android自带能支持的标签实在是太少了,而且连</br>都不能带斜杠得写成<br>不然不能正常得换行。

Spannable setSpan用到的这个类,百度多了解下

Android中为TextView增加自定义的HTML标签的更多相关文章

  1. Android中使用ListView绘制自定义表格(2)

    上回再写了<Android中使用ListView绘制自定义表格>后,很多人留言代码不全和没有数据样例.但因为项目原因,没法把源码全部贴上来.近两天,抽空简化了一下,做了一个例子. 效果图如 ...

  2. Android中设置TextView的颜色setTextColor

    tv.setTextColor(Color.parseColor("#FFFFFF")); tv.setTextColor(Color.WHITE); tv.setTextColo ...

  3. 【原创】如何在Android中为TextView动态设置drawableLeft等

    如何在Android中为TextView动态设置drawableLeft等   两种方式:   方式1:手动设置固有边界 Drawable drawable = getResources().getD ...

  4. 【转】Android中设置TextView的颜色setTextColor--代码中设置字体颜色

    原文网址:http://www.cnblogs.com/myphoebe/archive/2012/01/06/2314728.html android中设置TextView的颜色有方法setText ...

  5. 【转】Android中设置TextView的颜色setTextColor

    原文网址:http://www.cnblogs.com/myphoebe/archive/2012/01/06/2314728.html android中设置TextView的颜色有方法setText ...

  6. [转]Android中设置TextView的颜色setTextColor

    [转自]http://txlong-onz.iteye.com/blog/1249609 Android中设置TextView的颜色setTextColor android中设置TextView的颜色 ...

  7. Android中对TextView中的部分内容的字体样式的设置方法

    Android中的TextView中内容,有时候需要对其部分内容添加下划线和颜色操作: String str = "回复 " + uname + " 的评论: " ...

  8. Android中利用ViewHolder优化自定义Adapter的典型写法

    利用ViewHolder优化自定义Adapter的典型写法 最近写Adapter写得多了,慢慢就熟悉了. 用ViewHolder,主要是进行一些性能优化,减少一些不必要的重复操作.(WXD同学教我的. ...

  9. Android中使用SurfaceView+MediaPlayer+自定义的MediaController实现自定义的视屏播放器

    效果图如下: (PS本来是要给大家穿gif动态图的,无奈太大了,没法上传) 功能实现:暂停,播放,快进,快退,全屏,退出全屏,等基本功能 实现的思路: 在主布局中放置一个SurfaceView,在Su ...

随机推荐

  1. Python Scrapy爬虫框架之初次使用

    此篇博客为本人对小甲鱼的课程的总结. 关于Scrapy的安装网上都有方法,这里便不再叙述. 使用Scrapy抓取一个网站一共需要四个步骤: 0.创建一个Scrapy项目: 1.定义Item容器: 2. ...

  2. 洛谷 P2765 魔术球问题 (dinic求最大流,最小边覆盖)

    P2765 魔术球问题 题目描述 «问题描述: 假设有n根柱子,现要按下述规则在这n根柱子中依次放入编号为1,2,3,...的球. (1)每次只能在某根柱子的最上面放球. (2)在同一根柱子中,任何2 ...

  3. xgboost&lightgbm调参指南

    本文重点阐述了xgboost和lightgbm的主要参数和调参技巧,其理论部分可见集成学习,以下内容主要来自xgboost和LightGBM的官方文档. xgboost Xgboost参数主要分为三大 ...

  4. linux 的环境变量的配置文件

    原文:https://www.cnblogs.com/yuemw/p/8081219.html ---------------------------------------------------- ...

  5. SpringMVC——问题汇总

    1.html页面ajax请求/login.后台报: Request method 'GET' not supported 原因: 后台请求使用了method = POST,但是url请求没有formd ...

  6. Django的 select_related 和 prefetch_related 函数对 QuerySet 查询的优化

    引言 在数据库存在外键的其情况下,使用select_related()和prefetch_related()很大程度上减少对数据库的请求次数以提高性能 1.实例准备 模型: from django.d ...

  7. CSS效果篇--纯CSS+HTML实现checkbox的思路与实例

    checkbox应该是一个比较常用的html功能了,不过浏览器自带的checkbox往往样式不怎么好看,而且不同浏览器效果也不一样.出于美化和统一视觉效果的需求,checkbox的自定义就被提出来了. ...

  8. js文件中使用el表达式问题

    作者:Sang 单独js文件不能用el表达式. 首先,JSP是由服务端执行的,EL表达式自然也由服务端解析执行,因此如果EL所在的脚本在JSP页面内,它是可以获取到值的,这个值在服务器端返回到浏览器端 ...

  9. IAR astyle代码美化

    Menu Text填写上我们期望的名字,我们可以填入格式化代码,command内填入AStyle的地址, Argument内填入参数:  --style=ansi -s4 -S -Y  -D -xe ...

  10. Codeforces Round #589 (Div. 2) A. Distinct Digits

    链接: https://codeforces.com/contest/1228/problem/A 题意: You have two integers l and r. Find an integer ...