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的源码解析,就下次再说了~~~

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
手机页面模版html5码30个合集: ‘10【导航看上去不错】越野e族中国越野触屏版手机wap汽车网站模板下载 13【很适合新手】仿亞普達手机wap旅游网站模板下载 (2) 15【很适合新手学习】仿coffee手机wap企业网站模板下载 18【很适合新手学习】仿住 - 亞普達手机wap旅游网站模板下载 19【很有创意网站】礼物搜手机触屏版手机wap购物网站模板下载 1【32赞HTM5响应式系列之右侧多级滑动式】仿Slideby触屏版html5响应式手机wap网站模板 22【简单很适合新手】仿亞普達溫泉會館手机wap旅游网站模板下载 23【列表多种形式展示】仿生意街触屏版html5手机wap购物网站模板 26【手机app iso7风格之页面向右滑动切换结婚模板】结婚婚庆触屏版html5响应式手机app网站模板下载 27【淘宝客必备】购物客单页触屏版手机wap购物网站模板 2【32赞HTML5响应式系列之订餐模板】微官网美食订餐html5触屏响应式手机wap网站订餐模板 38【医院高档次模板】仿珠海平安整形美容医院触屏版html5手机wap医院网站模板下载 39【游戏模板敢称第一好】仿玩机岛触屏版html5手机wap游戏网站模板下载 40TouchScroll插件制作幻灯切换iPhone手机wap网站特效 45帮5买触屏版手机wap淘宝客购物网站模板下载 51仿7881触屏版游戏交易平台手机wap游戏网站模板 52仿GoMobile触屏版html5响应式手机app网站模板下载-懒人模板 56仿SAEKO微官网html5手机网站模板 57仿UG生活网触屏版手机wap网址导航网站模板 5【32赞HTML5响应式系列之小情绪风格】仿Epsilon Elements触屏版html5响应式手机wap网站模板下载 60仿诚信中国触屏版手机wap购物网站模板 61仿海王星辰网上药店触屏版手机wap健康购物网站模板 65仿金盛集团官方触屏版手机wap企业网站模板 66仿晋优惠触屏版手机wap购物网站模板 69仿楷维留学指南触屏版手机wap考试培训网站模板 6【不看绝对后悔】仿凤凰汽车触屏版html5手机wap汽车网站模板下载 70仿昆山看房网手机触屏版手机wap房产网站模板 73仿驱动之家触屏版手机wap硬件网站模板 74仿手机号码归属地查询触屏版手机wap查询网站模板码 9【超炫购物模板】仿拍鞋网商城首页触屏版html5手机wap购物网站模板
Android Studio中的图文可以通过使用TextView和SpannableString来实现。SpannableString是一个可以设置不同样式的字符串类,可以用来设置文字的颜色、大小、字体等属性。 以下是一个简单的例子,展示如何在TextView中实现图文: 1. 在布局文件中添加一个TextView: ``` <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> ``` 2. 在Java代码中获取TextView并设置SpannableString: ``` TextView textView = findViewById(R.id.textView); // 创建一个SpannableString对象 SpannableString spannableString = new SpannableString("这是一段文字和图片的例子"); // 创建一个ImageSpan对象,用来设置图片 Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); ImageSpan imageSpan = new ImageSpan(drawable); // 将图片插入到SpannableString中 spannableString.setSpan(imageSpan, 6, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // 设置TextView的文本 textView.setText(spannableString); ``` 在上面的代码中,我们首先创建了一个SpannableString对象,然后创建了一个ImageSpan对象,用来设置图片。接着,我们将图片插入到SpannableString中,并设置了图片的位置。最后,我们将SpannableString设置为TextView的文本。 这样,就可以在TextView中实现图文了。当然,这只是一个简单的例子,实际上可以通过SpannableString来实现更复杂的图文效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值