通常,当我们需要在TextView中显示服务器端返回的Html内容时,就会用到Html.fromHtml(String source)这个方法。Google在源码中默认的为我们提供了对一些Html标签的支持。
但是如果我们想要对已有的标签进行自定义展示,或者是有些标签还没有得到支持,那应该怎么做呢?


Html源码分析

我们知道,使用Html.fromHtml(String source)这个方法时,我们需要将带有Html标签的文本作为参数传递过来。那么在Html这个类中必然会有对不同的标签进行判断,并添加样式的方法。通过查找,主要是由两个方法来完成的

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);
    }
}

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);
    }
}

因为Html标签总是成对出现,所以有handleStartTagHandleEndTag两个方法。
其实在这两个方法中,都是对一些标签进行了判断,在调用start(SpannableStringBuilder text, Class kind, Object repl)end(SpannableStringBuilder text, Class kind, Object repl)这两个方法。

private static void start(SpannableStringBuilder text, Object mark) {
    int len = text.length();
    text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK);
}

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);
    }
}

在这两个方法中,最核心的代码就是SpannableStringBuilder.setSpan(Object what, int start, int end, int flags)

Mark the specified range of text with the specified object. The flags determine how the span will behave when text is inserted at the start or end of the span's range.

通过查阅文档,我们可以知道这个方法是将text中指定的区域添加上样式,flag参数决定在指定范围前后插入文本时是否采用同样的样式

解决问题

现在我们知道了Html.fromHtml是如何来实现对Html标签的解析并且添加上样式。那么如何来解决我们最开始提出的问题呢?

  • 例: 搜索指定字段,服务器返回的搜索结果中,将字段添加标签返回。此时使用Html.from默认的样式是斜体,而我们需要给字段添加背景为黄色的高亮。

‘em’ 标签告诉浏览器把其中的文本表示为强调的内容。对于所有浏览器来说,这意味着要把这段文字用斜体来显示。
除强调之外,当引入新的术语或在引用特定类型的术语或概念时作为固定样式的时候,也可以考虑使用 ‘em’ 标签。

替换样式

Spannable source = (Spannable) Html.fromHtml(content);
  final StyleSpan[] styleSpans = source.getSpans(0, source.length(), StyleSpan.class);
  if (styleSpans != null && styleSpans.length > 0) {
      for (StyleSpan styleSpan : styleSpans) {
          if (Typeface.ITALIC == styleSpan.getStyle()) {
              source.setSpan(new BackgroundColorSpan(Color.YELLOW), source.getSpanStart(styleSpan),
                      source.getSpanEnd(styleSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
              source.removeSpan(styleSpan);
          }
      }
  }

通过setSpanremoveSpan,我们将原有的StyleSpan(Typeface.ITALIC)移除,转而替换为BackgroundColorSpan

自定义TagHandler

使用重载的Html.fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)方法

public class MyTagHandler implements TagHandler{
    private int sIndex = 0;  
    private  int eIndex=0;
    private final Context mContext;

    public MxgsaTagHandler(Context context){
        mContext=context;
    }

    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
        if (tag.toLowerCase().equals("eem")) {
            if (opening) {
                sIndex=output.length();
            }else {
                eIndex=output.length();
                output.setSpan(new BackgroundColorSpan(Color.YELLOW), source.getSpanStart(styleSpan),
                        source.getSpanEnd(styleSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
    }
}

使用自定义的TagHandler时需要注意的一点是,因为Html.fromHtml会先调用默认的逻辑来添加样式,此时是不会调用到我们自己实现的TagHandler中的逻辑的。这时候我们只能先将初始文本通过正则表达式进行替换。
所以当Html类中没有进行处理的Html标签,再考虑使用自定义TagHandler会是一个更好的选择。