在前一篇 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),这里就不看代码了,他主要做了如下操作:

  1. 首先theScanner是HTMLScanner类型,因此这个scan是调用的HTMLScanner的scan函数,传入了Reader与ScanHander,ScanHander是在Parser中实现的。
  2. scan中读取每一个字符,出去特殊字符,对每一个字符根据statetable表进行处理
  3. 遇到某些字符时调用save方法处理,这里主要每次查看输入buffer,当大于20个字符才进行处理。调用ScanHander的pcdata函数。
  4. pcdata函数处理空白字符,之后调用rectify函数,从函数名可以知道是修正的意思。
  5. rectify函数中处理一个Element链表,因此会处理多次,最后还调用restart或者push函数,restart中也会继续调用push函数。
  6. 在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("&lt;");        } else if (c == '>') {            out.append("&gt;");        } else if (c == '&') {            out.append("&amp;");        } 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("&nbsp;");                i++;            }            out.append(' ');        } else {            out.append(c);        }    }}

这里根据不同的span生成对应的html内容,最后将生成了html返回。

总结

这里只是初步的解析了整个流程,其中还有很多内容可以继续如果,比如TagSoup,可以自行去看看代码。看看TagSoup是怎么解析令人发狂的html内容的。

一般简单的效果,用html就可以了,这样相对来说代码量少,了解html的人很容易就能明白需要实现什么效果,如果有很复杂的效果或者功能,或者同一段文本需要多种效果与功能,就需要采用SpannableString来实现了。实际开发中需要根据需求来实现对应的效果。

更多相关文章

  1. Android中打电话的数据流程
  2. Android的service相关讲解
  3. [置顶] Android(安卓)MediaPlayer+Stagefright框架(音频)图解
  4. Android(安卓)子线程修改UI方法对比
  5. 如何制作Jar包并在android中调用jar包
  6. Android(安卓)存储设备管理 -- Vold
  7. Android错误处理——Android读取txt文件乱码解决方案
  8. LGame(Android及J2SE游戏引擎)入门示例——如何构建一个游戏
  9. 箭头函数的基础使用

随机推荐

  1. golang判断tcp是否断开的方法
  2. golang中.a文件是什么
  3. golang不开发gui吗
  4. golang gmssl编译不过
  5. golang是什么语言
  6. golang map为啥不并发
  7. golang中sort包如何实现
  8. emacs支持golang吗
  9. golang用户登录怎么做
  10. dart和golang区别