zoukankan      html  css  js  c++  java
  • Android中为TextView增加自定义的HTML标签

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

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

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

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

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

    [java] view plain copy
     
    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进行解析部分的代码:

    [java] view plain copy
     
    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.   
    52.     private void handleEndTag(String tag) {  
    53.         if (tag.equalsIgnoreCase("br")) {  
    54.             handleBr(mSpannableStringBuilder);  
    55.         } else if (tag.equalsIgnoreCase("p")) {  
    56.             handleP(mSpannableStringBuilder);  
    57.         } else if (tag.equalsIgnoreCase("div")) {  
    58.             handleP(mSpannableStringBuilder);  
    59.         } else if (tag.equalsIgnoreCase("em")) {  
    60.             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));  
    61.         } else if (tag.equalsIgnoreCase("b")) {  
    62.             end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));  
    63.         } else if (tag.equalsIgnoreCase("strong")) {  
    64.             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));  
    65.         } else if (tag.equalsIgnoreCase("cite")) {  
    66.             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));  
    67.         } else if (tag.equalsIgnoreCase("dfn")) {  
    68.             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));  
    69.         } else if (tag.equalsIgnoreCase("i")) {  
    70.             end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));  
    71.         } else if (tag.equalsIgnoreCase("big")) {  
    72.             end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));  
    73.         } else if (tag.equalsIgnoreCase("small")) {  
    74.             end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));  
    75.         } else if (tag.equalsIgnoreCase("font")) {  
    76.             endFont(mSpannableStringBuilder);  
    77.         } else if (tag.equalsIgnoreCase("blockquote")) {  
    78.             handleP(mSpannableStringBuilder);  
    79.             end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());  
    80.         } else if (tag.equalsIgnoreCase("tt")) {  
    81.             end(mSpannableStringBuilder, Monospace.class,  
    82.                     new TypefaceSpan("monospace"));  
    83.         } else if (tag.equalsIgnoreCase("a")) {  
    84.             endA(mSpannableStringBuilder);  
    85.         } else if (tag.equalsIgnoreCase("u")) {  
    86.             end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());  
    87.         } else if (tag.equalsIgnoreCase("sup")) {  
    88.             end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());  
    89.         } else if (tag.equalsIgnoreCase("sub")) {  
    90.             end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());  
    91.         } else if (tag.length() == 2 &&  
    92.                 Character.toLowerCase(tag.charAt(0)) == 'h' &&  
    93.                 tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {  
    94.             handleP(mSpannableStringBuilder);  
    95.             endHeader(mSpannableStringBuilder);  
    96.         } else if (mTagHandler != null) {  
    97.             mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);  
    98.         }  
    99.     }  


     

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

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

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


     

    上面这段代码,是对<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

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

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

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

    test
    然后进行节点的处理

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

    1
    看到这里的时候我的心里是奔溃的。。尼玛这都什么跟什么,怎么就支持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来进行判断每一个标签头和尾的位置,对里面的文本进行相对应的样式处理。然后让我们来运行一下代码试试

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

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

    四、总结

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

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

  • 相关阅读:
    制作自适应布局的模块及框架(转载)
    从今天起开始写博了
    工作中碰到的css问题解决方法
    标题写个什么好呢
    快速编写HTML(Zen conding)
    2013年1月21日记事
    opc 方面研究
    关于 部署方面研究 Visual Studio 2013
    intel AVX指令集
    关于 返回数据类型 后 加& 的作用
  • 原文地址:https://www.cnblogs.com/cfas/p/7814166.html
Copyright © 2011-2022 走看看