Android(安卓)Html解析
在前一篇 Android SpannableString浅析中我们采用html实现了文本处理的效果。当时设置部分的代码如下:
private void setText() { String originText = "#重磅消息#近日谷歌放出Android N的第二个开发者预览版(Developer Preview)"; String effect1 = "<font color='#FF0000'>#重磅消息#</font> <br> 近日谷歌放出Android " + "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>"; String effect2 = "<font color='#303F9F'>#重磅消息#</font> 近日谷歌放出Android " + "N的第二个开发者预览版<a href='http://developer.android.com/index.html'>(Developer Preview)</a>"; StringBuilder sb = new StringBuilder(originText); sb.append("<br><br><br><br>"); sb.append(effect1); sb.append("<br><br><br><br>"); sb.append(effect2); textView.setText(Html.fromHtml(sb.toString())); textView.setMovementMethod(LinkMovementMethod.getInstance());}
这里我们改变了部分文字的显示颜色,同时对另外一部分内容添加了点击事件的处理,并且加入了下划线。今天我就来看看Html代码的解析。
解析过程
在上面的代码中我们设置的时候调用了Html.fromHtml()函数,从这个函数就可以知道这个是将一段html内容解析成TextView可以展示的内容。我们就从这开始逐步看看该类做了哪些事情?能解析的内容是什么样的?
在调用这个函数之前,我们首先看看Html的构造函数private Html() { },可以看到被private修饰,说明在外部不能构造Html实例,我们看到代码中也没有任何返回实例的地方,因此这里的使用方式都是采取静态方法调用。
我们来看看fromHtml()函数:
public static Spanned fromHtml(String source) { return fromHtml(source, null, null);}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) { // Should not happen. throw new RuntimeException(e); } catch (org.xml.sax.SAXNotSupportedException e) { // Should not happen. throw new RuntimeException(e); } HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser); return converter.convert();}
Html.fromHtml函数继续调用了三参数的romHtml(String source, ImageGetter imageGetter,TagHandler tagHandler),参数分别为数据源,图片处理,tag处理 。首先构造了一个Parser实例,Parser在org.ccil.cowan.tagsoup包下,我本地的代码是不能导航到的,这里给该代码的一个连接Parser源码。
这里我们额外说一下TagSoup,我们来看看他的官方介绍:
This is the home page of TagSoup, 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: poor, 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. TagSoup also includes a command-line processor that reads HTML files and can generate either clean HTML or well-formed XML that is a close approximation to XHTML.
对应大致的意思就是:
TagSoup是Java语言编写一个解析Html的工具,他通过SAX引擎解析结构糟糕、令人抓狂的不规范HTML文档。TagSoup可以将一个HTML文档转换为结构良好的XML文档,方便开发人员对获取的HTML文档进行解析等操作。同时TagSoup提供了命令行程序,可以运行TagSoup来对HTML文档进行解析。
构造了Parser后调用了Parser.setProperty函数,传入了schemaProperty,这里是一个字符串,还传入了HTMLSchema对象,HTMLSchema对象罗列了HTML的所有属性节点,HTMLSchema也属于TagSoup,这里不对TagSoup做过多的介绍,知道他是干什么的就好了。
最后构造了一个HtmlToSpannedConverter实例,传入了上面传递进来的参数,数据源,imageGetter, tagHandler, parser,最后调用了HtmlToSpannedConverter的 convert函数。这里我们看这个类名就能大致知道该类做了什么操作,主要是将Html内容转换为Span对象。这里就又回到了前一篇的内容,最终处理的都是Span类型。
这里我们先看看HtmlToSpannedConverter的构造函数都干了什么?
public HtmlToSpannedConverter(String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser) { mSource = source; mSpannableStringBuilder = new SpannableStringBuilder(); mImageGetter = imageGetter; mTagHandler = tagHandler; mReader = parser;}
可以看到将传入的参数赋值给对应的变量,同时构造了一个SpannableStringBuilder对象,该对象与StringBuilder类型,StringBuilder主要是连接字符串,减少不必要的空间浪费,SpannableStringBuilder当然就是连接SpannableString,SpannableString的主要内容看前一篇 Android SpannableString浅析,我们继续看看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); } // Fix flags and range for paragraph-type markup. Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class); for (int i = 0; i < obj.length; i++) { int start = mSpannableStringBuilder.getSpanStart(obj[i]); int end = mSpannableStringBuilder.getSpanEnd(obj[i]); // If the last line of the range is blank, back off by one. if (end - 2 >= 0) { if (mSpannableStringBuilder.charAt(end - 1) == '\n' && mSpannableStringBuilder.charAt(end - 2) == '\n') { end--; } } if (end == start) { mSpannableStringBuilder.removeSpan(obj[i]); } else { mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); } } return mSpannableStringBuilder;}
这里首先调用了mReader. setContentHandler函数,mReader就是前面构造的Parser实例,之后调用了mReader.parse函数,将传入的source构造成一个InputSource对象。我们先去看看parse对象,之后再接着往下看。
public void parse(InputSource input) throws IOException, SAXException { setup(); Reader r = getReader(input); theContentHandler.startDocument(); theScanner.resetDocumentLocator(input.getPublicId(), input.getSystemId()); if (theScanner instanceof Locator) { theContentHandler.setDocumentLocator((Locator) theScanner); } if (!(theSchema.getURI().equals(""))) theContentHandler.startPrefixMapping(theSchema.getPrefix(), theSchema.getURI()); theScanner.scan(r, this);}
首先调用了setUp,这里主要做一些赋值,初始化操作。接着将传入的InputSource对象转换成一个Reader对象,接着调用了ContentHander的startDocument,这里就是调用的就是HtmlToSpannedConverter的startDocument,可以看到是一个空函数,啥都没有做。 这里我们主要来看看最主要的的部分theScanner.scan(r, this),这里就不看代码了,他主要做了如下操作:
- 首先theScanner是HTMLScanner类型,因此这个scan是调用的HTMLScanner的scan函数,传入了Reader与ScanHander,ScanHander是在Parser中实现的。
- scan中读取每一个字符,出去特殊字符,对每一个字符根据statetable表进行处理
- 遇到某些字符时调用save方法处理,这里主要每次查看输入buffer,当大于20个字符才进行处理。调用ScanHander的pcdata函数。
- pcdata函数处理空白字符,之后调用rectify函数,从函数名可以知道是修正的意思。
- rectify函数中处理一个Element链表,因此会处理多次,最后还调用restart或者push函数,restart中也会继续调用push函数。
- 在push函数中我们终于见到了属性的theContentHandler,调用了theContentHandler.startElement(namespace, localName, name, e.atts());这里是在HtmlToSpannedConverter中实现的,因此这里实际上调用的是HtmlToSpannedConverter的startElement函数,startElement函数又继续调用了handleStartTag函数。
我们来看看handleStartTag函数:
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); }}
从上述的代码中也可以看出我们能够解析html中的那些标签,这里仅仅只处理了所有Element的start标签,还没有处理end标签,start主要做对text设置了Span,初始与结束为止都是同一个,设置了一个空了类,作为标识对象,但是对于标签p,div,img等做了不同的设置,尤其是img这里主要调用了设置替换图标,这个功能是前面ImageGetter来实现的。
private static void start(SpannableStringBuilder text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);}
上面只是做了start标签,那end标签又做了上面,在前面我们说rectify调用了push进行压栈,当时略过了其他的代码,这里还做了另一项处理,当一个Element扫描完后还进行了pop出栈,pop中调用了endElement函数,这里实际调用了HtmlToSpannedConverter的endElement函数,endElement函数中又继续调用了handleEndTag函数。
我们来看看handleEndTag函数:
private void handleEndTag(String tag) { if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("strong")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("b")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } else if (tag.equalsIgnoreCase("em")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("cite")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("dfn")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("i")) { end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC)); } else if (tag.equalsIgnoreCase("big")) { end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f)); } else if (tag.equalsIgnoreCase("small")) { end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f)); } else if (tag.equalsIgnoreCase("font")) { endFont(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("blockquote")) { handleP(mSpannableStringBuilder); end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan()); } else if (tag.equalsIgnoreCase("tt")) { end(mSpannableStringBuilder, Monospace.class, new TypefaceSpan("monospace")); } else if (tag.equalsIgnoreCase("a")) { endA(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("u")) { end(mSpannableStringBuilder, Underline.class, new UnderlineSpan()); } else if (tag.equalsIgnoreCase("sup")) { end(mSpannableStringBuilder, Super.class, new SuperscriptSpan()); } else if (tag.equalsIgnoreCase("sub")) { end(mSpannableStringBuilder, Sub.class, new SubscriptSpan()); } else if (tag.length() == 2 && Character.toLowerCase(tag.charAt(0)) == 'h' && tag.charAt(1) >= '1' && tag.charAt(1) <= '6') { handleP(mSpannableStringBuilder); endHeader(mSpannableStringBuilder); } else if (mTagHandler != null) { mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); } }
这里与handleStartTag成对处理,主要做了替换处理,根据stat中传入的类标识,重新设置span对象。
private static void end(SpannableStringBuilder text, Class kind, Object repl) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { text.setSpan(repl, where, len,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } }
这里的kind就是前面mark,repl为重新替换的span对象,这里就看到主要可以使用如下的span对象:StyleSpan,RelativeSizeSpan,QuoteSpan,UnderlineSpan,SuperscriptSpan。可以看到他比SpannableString能够处理的东西要少很多,我们这里我们可以看到header里面进行了两种span处理。最后如果所有的节点都没有匹配,如果你自己实现了mTagHandler,则采用mTagHandler进行处理。
这里我们再回到convert函数,parse处理完后,继续处理了mSpannableStringBuilder,循环处理设置的span,忽略’\n’换行符,之后如果span的start与end为同一个位置,说明该节点没有任何内容处理,将该span remove掉。最后将处理完成的SpannableStringBuilder返回给TextView进行展示。
反解
上面将html转换成了SpannableStringBuilder,Html同时还能降SpannableStringBuilder内容转换为html内容。这里主要调用toHtml函数。
public static String toHtml(Spanned text) { StringBuilder out = new StringBuilder(); withinHtml(out, text); return out.toString();}private static void withinHtml(StringBuilder out, Spanned text) { int len = text.length(); int next; for (int i = 0; i < text.length(); i = next) { next = text.nextSpanTransition(i, len, ParagraphStyle.class); ParagraphStyle[] style = text.getSpans(i, next, ParagraphStyle.class); String elements = " "; boolean needDiv = false; for(int j = 0; j < style.length; j++) { if (style[j] instanceof AlignmentSpan) { Layout.Alignment align = ((AlignmentSpan) style[j]).getAlignment(); needDiv = true; if (align == Layout.Alignment.ALIGN_CENTER) { elements = "align=\"center\" " + elements; } else if (align == Layout.Alignment.ALIGN_OPPOSITE) { elements = "align=\"right\" " + elements; } else { elements = "align=\"left\" " + elements; } } } if (needDiv) { out.append("<div ").append(elements).append(">"); } withinDiv(out, text, i, next); if (needDiv) { out.append("</div>"); } }}
toHtml函数中又调用了withinHtml函数,withinHtml函数中循环处理了span对象。如果是AlignmentSpan对象则外层嵌套一层div,之后调用withinDiv继续处理。
private static void withinDiv(StringBuilder out, Spanned text, int start, int end) { int next; for (int i = start; i < end; i = next) { next = text.nextSpanTransition(i, end, QuoteSpan.class); QuoteSpan[] quotes = text.getSpans(i, next, QuoteSpan.class); for (QuoteSpan quote : quotes) { out.append("<blockquote>"); } withinBlockquote(out, text, i, next); for (QuoteSpan quote : quotes) { out.append("</blockquote>\n"); } }}
withinDiv中继续对文本进行处理。最终会调用withinParagraph对文本进行处理。withinParagraph处理了对应的span对象:
private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) { int next; for (int i = start; i < end; i = next) { next = text.nextSpanTransition(i, end, CharacterStyle.class); CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class); for (int j = 0; j < style.length; j++) { if (style[j] instanceof StyleSpan) { int s = ((StyleSpan) style[j]).getStyle(); if ((s & Typeface.BOLD) != 0) { out.append("<b>"); } if ((s & Typeface.ITALIC) != 0) { out.append("<i>"); } } if (style[j] instanceof TypefaceSpan) { String s = ((TypefaceSpan) style[j]).getFamily(); if ("monospace".equals(s)) { out.append("<tt>"); } } if (style[j] instanceof SuperscriptSpan) { out.append("<sup>"); } if (style[j] instanceof SubscriptSpan) { out.append("<sub>"); } if (style[j] instanceof UnderlineSpan) { out.append("<u>"); } if (style[j] instanceof StrikethroughSpan) { out.append("<strike>"); } if (style[j] instanceof URLSpan) { out.append("<a href=\""); out.append(((URLSpan) style[j]).getURL()); out.append("\">"); } if (style[j] instanceof ImageSpan) { out.append("<img src=\""); out.append(((ImageSpan) style[j]).getSource()); out.append("\">"); i = next; } if (style[j] instanceof AbsoluteSizeSpan) { out.append("<font size =\""); out.append(((AbsoluteSizeSpan) style[j]).getSize() / 6); out.append("\">"); } if (style[j] instanceof ForegroundColorSpan) { out.append("<font color =\"#"); String color = Integer.toHexString(((ForegroundColorSpan)style[j]).getForegroundColor() + 0x01000000); while (color.length() < 6) { color = "0" + color; } out.append(color); out.append("\">"); } } withinStyle(out, text, i, next); for (int j = style.length - 1; j >= 0; j--) { if (style[j] instanceof ForegroundColorSpan) { out.append("</font>"); } if (style[j] instanceof AbsoluteSizeSpan) { out.append("</font>"); } if (style[j] instanceof URLSpan) { out.append("</a>"); } if (style[j] instanceof StrikethroughSpan) { out.append("</strike>"); } if (style[j] instanceof UnderlineSpan) { out.append("</u>"); } if (style[j] instanceof SubscriptSpan) { out.append("</sub>"); } if (style[j] instanceof SuperscriptSpan) { out.append("</sup>"); } if (style[j] instanceof TypefaceSpan) { String s = ((TypefaceSpan) style[j]).getFamily(); if (s.equals("monospace")) { out.append("</tt>"); } } if (style[j] instanceof StyleSpan) { int s = ((StyleSpan) style[j]).getStyle(); if ((s & Typeface.BOLD) != 0) { out.append("</b>"); } if ((s & Typeface.ITALIC) != 0) { out.append("</i>"); } } } } if (nl == 1) { out.append("<br>\n"); return false; } else { for (int i = 2; i < nl; i++) { out.append("<br>"); } return !last; }}private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) { for (int i = start; i < end; i++) { char c = text.charAt(i); if (c == '<') { out.append("<"); } else if (c == '>') { out.append(">"); } else if (c == '&') { out.append("&"); } else if (c >= 0xD800 && c <= 0xDFFF) { if (c < 0xDC00 && i + 1 < end) { char d = text.charAt(i + 1); if (d >= 0xDC00 && d <= 0xDFFF) { i++; int codepoint = 0x010000 | (int) c - 0xD800 << 10 | (int) d - 0xDC00; out.append("&#").append(codepoint).append(";"); } } } else if (c > 0x7E || c < ' ') { out.append("&#").append((int) c).append(";"); } else if (c == ' ') { while (i + 1 < end && text.charAt(i + 1) == ' ') { out.append(" "); i++; } out.append(' '); } else { out.append(c); } }}
这里根据不同的span生成对应的html内容,最后将生成了html返回。
总结
这里只是初步的解析了整个流程,其中还有很多内容可以继续如果,比如TagSoup,可以自行去看看代码。看看TagSoup是怎么解析令人发狂的html内容的。
一般简单的效果,用html就可以了,这样相对来说代码量少,了解html的人很容易就能明白需要实现什么效果,如果有很复杂的效果或者功能,或者同一段文本需要多种效果与功能,就需要采用SpannableString来实现了。实际开发中需要根据需求来实现对应的效果。
更多相关文章
- Android中打电话的数据流程
- Android的service相关讲解
- [置顶] Android(安卓)MediaPlayer+Stagefright框架(音频)图解
- Android(安卓)子线程修改UI方法对比
- 如何制作Jar包并在android中调用jar包
- Android(安卓)存储设备管理 -- Vold
- Android错误处理——Android读取txt文件乱码解决方案
- LGame(Android及J2SE游戏引擎)入门示例——如何构建一个游戏
- 箭头函数的基础使用