Android TextView之Html.fromHtml学习(二)

本文详细探讨了Android TextView中Html.fromHtml方法的实现,特别是如何处理HTML标签,尤其是图片(img)标签的解析。通过分析源码,揭示了当不提供自定义标签处理器时,图片无法正确显示的原因。同时,提到了通过实现TagHandler接口来自定义标签解析,以实现更灵活的文本样式定制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一篇文章简单介绍了Html.fromHtml的用法,继续分析其中的实现,该方法的第三个参数TagHandler ,该参数为Html里的一个内部静态接口,该接口里边定义了一个方法

public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader);

该方法接收四个参数,我们重点看一下最后一个参数XMLReader xmlReader ,是不是很眼熟?在XML解析中我们遇到过。我们再来看一下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) {
            // 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();
    }

第一行Parser parser = new Parser(); 这里提供一个关于该类的链接,有兴趣的同学可以自己查看源码,源码比较多:org.ccil.cowan.tagsoup.Parser,这里我们重点看一下converter.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);
        }

这里贴该方法的代码片段,其中mReader 就是在构建HtmlToSpannedConverter 对象的时候传过来的Parser对象。HtmlToSpannedConverter 类实现了ContentHandler 接口,convert 方法的第一行就是mReader.setContentHandler(this); 此时执行mReader.parse(new InputSource(new StringReader(mSource))); 就可以对传入的Html格式的字符串进行解析了。再来看

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

在解析到每一个头结点的时候,都会调用一个叫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标签(不是所有Html标签),之前我们说过Html.fromHtm方法并不是支持所有标签的。里面绝大多数标签都会执行handlePstart ,这个就不具体分析了,因为这些标签我们使用的时候都能正常显示,我们重点看一下

else if (tag.equalsIgnoreCase("img")) {
            startImg(mSpannableStringBuilder, attributes, mImageGetter);
        } 

我们还是进入startImg 的内部来看看具体实现

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

        if (d == null) {
            d = Resources.getSystem().
                    getDrawable(com.android.internal.R.drawable.unknown_image);
            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        }

        int len = text.length();
        text.append("\uFFFC");

        text.setSpan(new ImageSpan(d, src), len, text.length(),
                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

这段代码很简单,首先获取img 标签里src 属性的值,这个属性放的就是图片的URL,然后判断img s是否为空,这个img是Html.ImageGetter的一个对象,如果img不为空的话,就会调用Html.ImageGettergetDrawable 方法来获取一个Drawable 对象。看到这里我们就会很熟悉了,之前的文章里,我们已经实现了自己的Html.ImageGetter 类,并在getDrawabl 里通过网络加载了图片。如果img 为空的话,就会加载一个系统内置的Drawable对象:

if (d == null) {
            d = Resources.getSystem().
                    getDrawable(com.android.internal.R.drawable.unknown_image);
            d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
        }

这就可以解释,如果我们在调用一个参数的Html.fromHtml 方式时,所有的img 标签处只是显示一个很小的正方形图片,而不是真正我们需要的图片。至此我们就完成了图文的正常显示。
最后呢,如果传过来的标签并不包含在我们上述代码的判断中,则会调用

else if (mTagHandler != null) {
            mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
        }

这里也很眼熟吧,mTagHandler.handleTag 就是第三个参数,这里我们可以通过自己实现该类来自行定制想显示的样式,会比较灵活。
整个Html.fromHtml 的实现就简单分析完了,一般我们定制一个TextView上显示不同的样式文字的时候是通过定义SpannedStringHtml.fromHtml 方式。SpannedString 实现了Spanned 接口,而Html.fromHtml 返回一个Spanned 接口的对象,所以两个的原理都一样,只是在定制文字的方式上有区别。
这篇文章也可以告一段落了,文章中如果有错误之处呢,还希望大家能指出!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值