android.text.Html源码解析-再也不用担心图文混排什么的了

这些天,产品需求给客户端发送的文本是可以点击的,而且还可以跳转指定的app内的界面。对android熟悉的都知道UrlSpan,ClickableSpan这些类,这些类主要是让textview实现不同样式而设置的类,还有一个特别的类Html,这个类使用sax解析解析textview的文本,使textview支持Html标签语言。
这个app内部的任意跳转不是说给textview添加一个click事件,然后startactivity了事,而是由服务端发送的一段文字,在app端也是可以点击,而且跳转的界面是由服务端发来的数据控制,下面进入正题。

Html类的简单使用

html使用很简单,代码如下,这样就可以让textview的内容支持Html标签语言了,但是支持的标签只是有限的:

TextView textView = (TextView) findViewById(R.id.text);
textView.setText(Html.fromHtml(source));
textView.setMovementMethod(LinkMovementMethod.getInstance());

其支持的标签有:
br: 换行
a: 链接
p:段落
div
strong:粗体
b:粗体
em
cite
dfn
i:斜体
big:大字体 相对于当前字体的1.25
small:小字体,相对于当前字体的0.8
font:字体,支持 colorface 属性
blockquote
tt
u:下划线
sup:上标
sub:下标
h1-6
img:支持图片,但是必须有一个Html.ImageGetter对象可以获取图片对象

Html源码解析

Html类是处于包android.text下的一个text处理工具类

public class Html {
    private Html() { }
    public static Spanned fromHtml(String source) {
        return fromHtml(source, null, null);
    }

私有构造方法~~,好自私。看fromHtml方法,发现它调用的是自己的重载方法:

 public static Spanned fromHtml(String source, ImageGetter imageGetter,
                                   TagHandler tagHandler) {
        Parser parser = new Parser();
        try {
            parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
        } catch (org.xml.sax.SAXNotRecognizedException e) {
            throw new RuntimeException(e);
        } catch (org.xml.sax.SAXNotSupportedException e) {
            throw new RuntimeException(e);
        }

        HtmlToSpannedConverter converter =
                new HtmlToSpannedConverter(source, imageGetter, tagHandler,
                        parser);
        return converter.convert();
    }

fromHtml它先new了一个Parser,也就是标签解析器。注意这个Parser不是android里Sax解析包(org.xml.sax.Parser)里的Parser。这个Parser是一个interface,不能直接new对象的,这个Parser是包org.ccil.cowan.tagsoup下的,很奇怪的这个包对我们是不可见的,但是我们可以去findjar网站上下载链接地址http://www.findjar.com/jar/org.ccil.cowan.tagsoup/jars/tagsoup-1.1.3.jar.html

tagsoup的简单介
TagSoup is a SAX-compliant parser written in Java that, instead of parsing well-formed or valid XML, parses HTML as it is found in the wild: nasty and brutish, though quite often far from short. TagSoup is designed for people who have to process this stuff using some semblance of a rational application design. By providing a SAX interface, it allows standard XML tools to be applied to even the worst HTML.

我们都知道Html标签语言是不标准的,在android中,我们知道的解析有xmlpull解析,但是这个解析必须是格式良好的xml,否则解析失败,使用这个库的parser就可以解析非良好格式的xml,或者说是标签语言

然后fromHtml创建了一个HtmlToSpannedConverter对象,使用它的convert方法。

HtmlToSpannedConverter类

这个类有5个成员变量:mSource,就是文本内容,mReader文本解析器,mSpannableStringBuilder,就是将文本中的标签语言读取出来后,给文本设置样式后返回给textview的span,mImageGetter是图片获取的接口对象(如果有img标签,那么会调用这个对象的getDrawable方法获取一个Drawable),mTagHandler是当出现不认识的标签的时候的标签处理器(觉得很鸡肋,后面在吐槽吧~~~)

class HtmlToSpannedConverter implements ContentHandler {
    private String mSource;
    private XMLReader mReader;
    private SpannableStringBuilder mSpannableStringBuilder;
    private Html.ImageGetter mImageGetter;
    private Html.TagHandler mTagHandler;

    public HtmlToSpannedConverter(
            String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler,
            Parser parser) {
        mSource = source;
        mSpannableStringBuilder = new SpannableStringBuilder();
        mImageGetter = imageGetter;
        mTagHandler = tagHandler;
        mReader = parser;
    }

看关键的convert方法。

  public Spanned convert() {
        mReader.setContentHandler(this);
        try {
            mReader.parse(new InputSource(new StringReader(mSource)));
        } catch (IOException e) {
            // We are reading from a string. There should not be IO problems.
            throw new RuntimeException(e);
        } catch (SAXException e) {
            // TagSoup doesn't throw parse exceptions.
            throw new RuntimeException(e);
        }
        ......
        return mSpannableStringBuilder;
    }

mReader.setContentHandler(this),将自己作为内容处理者传入了,然后parse数据,然后将处理后的结果返回。HtmlToSpannedConverter是实现了ContentHandler接口的~,下面看这个类的内容处理

  public void startElement(String uri, String localName, String qName, Attributes attributes)
            throws SAXException {
        handleStartTag(localName, attributes);
    }

    public void endElement(String uri, String localName, String qName) throws SAXException {
        handleEndTag(localName);
    }

处理tag都在这两个方法里了~

  private void handleStartTag(String tag, Attributes attributes) {
        if (tag.equalsIgnoreCase("br")) {
            // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
            // so we can safely emite the linebreaks when we handle the close tag.
        } else if (tag.equalsIgnoreCase("p")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("div")) {
            handleP(mSpannableStringBuilder);
        } else if (tag.equalsIgnoreCase("strong")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("b")) {
            start(mSpannableStringBuilder, new Bold());
        } else if (tag.equalsIgnoreCase("em")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("cite")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("dfn")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("i")) {
            start(mSpannableStringBuilder, new Italic());
        } else if (tag.equalsIgnoreCase("big")) {
            start(mSpannableStringBuilder, new Big());
        } else if (tag.equalsIgnoreCase("small")) {
            start(mSpannableStringBuilder, new Small());
        } else if (tag.equalsIgnoreCase("font")) {
            startFont(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("blockquote")) {
            handleP(mSpannableStringBuilder);
            start(mSpannableStringBuilder, new Blockquote());
        } else if (tag.equalsIgnoreCase("tt")) {
            start(mSpannableStringBuilder, new Monospace());
        } else if (tag.equalsIgnoreCase("a")) {
            startA(mSpannableStringBuilder, attributes);
        } else if (tag.equalsIgnoreCase("u")) {
            start(mSpannableStringBuilder, new Underline());
        } else if (tag.equalsIgnoreCase("sup")) {
            start(mSpannableStringBuilder, new Super());
        } else if (tag.equalsIgnoreCase("sub")) {
            start(mSpannableStringBuilder, new Sub());
        } else if (tag.length() == 2 &&
                   Character.toLowerCase(tag.charAt(0)) == 'h' &&
                   tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
            handleP(mSpannableStringBuilder);
            start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
        } else if (tag.equalsIgnoreCase("img")) {
            startImg(mSpannableStringBuilder, attributes, mImageGetter);
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
        }
    }

这个方法对tag进行判断,比如:当是img标签的时候,调用startImg方法,把mImageGetter传入,通过调用getDrawable方法获取Drawable,然后使用ImageSpan设置给text展示

private static void startImg(SpannableStringBuilder text,
                                 Attributes attributes, Html.ImageGetter img) {
        String src = attributes.getValue("", "src");
        Drawable d = null;
        if (img != null) {
            d = img.getDrawable(src);
        }
      ....
        text.setSpan(new ImageSpan(d, src), len, text.length(),
                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

最后,如果不识别的标签就调用mTagHandler.handleTag方法处理,所以当需要自定义一些样式(比如将字体放大几倍等)的时候就可以通过自己定义的标签来处理。
吐槽:
注意 android.text.handleTag(true, tag, mSpannableStringBuilder, mReader);handTag方法传入的参数有
true:表示是否是标签开始
tag:标签名字
mSpannableStringBuilder:要返回的对象~~~
mReader:解析器
handleStartTag(String tag, Attributes attributes)的时候不是还有一个attributes,为什么不把已经读取到的attributes也传入呢?这就是我想要吐槽的了,明明已经读取到了的参数,问什么要舍弃了呢???

这就涉及到app里的需求了,当某个text被点击后跳转指定的界面,也就是activity,但是跳转到哪一个界面一般是需要有参数的,一般的intent里的参数如果能像标签语言的属性一样放置在标签里那么就完美解决问题了,但是到这一步,google工程师竟然把attributes给丢了!!!!让人觉得这个接口做的不是很完善。也许你会想到不是还有一个参数mReader么,给mReader再setContentHandler不就可以了么?,但是这会使得前面的标签解析完全被覆盖,html的正常标签都支持不了了(有兴趣的可以去试试)。
最后推测是因为在标签结束的时候也需要调用这个方法,而标签结束endElement(String uri, String localName, String qName) 方法是没有attributes参数的(没有不会传入一个null么!!)。

简而言之,就是Html支持自定义标签,但是不支持自定义标签里的属性!!!

为了解决这个问题只能写自己的Html类了,实际上大部分的代码都可以从android.text.Html类中copy过来,唯一需要修改的就是TagHandler接口的handletag方法(思路很简单,调用handletag的时候把获取到的attributes传入就好了)

....
public class MyHtml {

    private MyHtml() { }

    public static interface MyTagHandler {
        /**
         * opening true:tag开始,attributes可能有值
         * opening false:tag结束,attributes为空
         * 
         */
        public void handleTag(boolean opening, String tag,
                SpannableStringBuilder output, XMLReader xmlReader,Attributes attributes);
    }
.....

}
class HtmlToSpannedConverter implements ContentHandler {
    .....
    private void handleStartTag(String tag, Attributes attributes) {
        if (tag.equalsIgnoreCase("br")) {
           ......
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader,attributes);
        }
    }

    private void handleEndTag(String tag) {
        if (tag.equalsIgnoreCase("br")) {
         ......
        } else if (mTagHandler != null) {
            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader, null);
        }
    }
  .....
}

如上,就可以实现html基本的标签也支持,然后你自定义的标签也支持了!(当然,使用 android.text.Html类也可以支持自定义标签,但是这个自定义标签是不支持属性的!)
这里有几个需要注意:在前面说过了,parser类不是android原生的类,是属于org.ccil.cowan.tagsoup包下的,所以需要去下载这个Tagsoup包。

继续还没完成的HtmlToSpannedConverter类的解读,刚刚才说到了tag标签的开始处理,下面看看具体的处理:(以a标签为例):startA就是a标签开始后调用的方法

 private static void startA(SpannableStringBuilder text, Attributes attributes) {
        String href = attributes.getValue("", "href");

        int len = text.length();
        text.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
    }

这里就是将属性获取出来,然后new了一个Href对象,放置在text的span中,当tag结束的时候再从中读取出来,看endA方法

   private static void endA(SpannableStringBuilder text) {
        int len = text.length();
        Object obj = getLast(text, Href.class);
        int where = text.getSpanStart(obj);

        text.removeSpan(obj);

        if (where != len) {
            Href h = (Href) obj;

            if (h.mHref != null) {
                text.setSpan(new URLSpan(h.mHref), where, len,
                             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }

getLast方法就是从SpannableStringBuilder中获取最后一个Href对象,然后还是通过setSpan方法给SpannableStringBuilder设置进去了,startA的时候给SpannableStringBuilder添加的是一个没有效果的Href(这个Href可以是任意Object,相当于是给这部分做了一个标记一样,但是如果这个Object不是span类型,那么是没有任何显示效果的:比如字体颜色大小等),endA就取出Href,然后获取参数,设置有效果的span(比如URLSpan等)给SpannableStringBuilder。最后将SpannableStringBuilder返回设置给TextView就好了。

下面是getLast方法的源码;很简单就不解释了

   private static Object getLast(Spanned text, Class kind) {
        /*
         * This knows that the last returned object from getSpans()
         * will be the most recently added.
         */
        Object[] objs = text.getSpans(0, text.length(), kind);

        if (objs.length == 0) {
            return null;
        } else {
            return objs[objs.length - 1];
        }
    }

这样基本的这个Html类以及相关的HtmlToSpannedConverter类也就没什么秘密了,Html还是比较强大的,还支持toHtml,也就是将Span 还原成String
简而言之,Html这个类就两个功能 String —> Span 和 Span —-> String
Span —-> String功能使用的比较少,就不说了。
看到这里,相信读者自己也可以编写自己的Html类实现textview的各种效果了
关于Span的源码解析,就下次再说了~~~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值