前言
Android 中 TextView 可以实现简单的HTML解析,将 Html 文本封装为 Spannable 数据实现图文混排等富文本效果,但是问题很多。
- 1、Android系统中提供的解析能力不够强,提供的CSS样式支持不足,对于css 属性的解析和支持很弱
- 2、不支持多个多种css 样式同时解析
- 3、SDK 中提供的 Html.TagHandler 无法获取到标签属性
- 4、无法支持自定义Html 标签
- 5、无法支持自定义CSS属性
基于以上缺陷,如果我们想在TextView中支持更丰富的样式,相对来说SpannableString也能实现,但是SpannableString有个明显的缺点,通用性不够高且不够自动化,但是作为Html,他的通用性是目前来说比较高的,同时借助Html的实现,自动话能力也很高,我们常用的markdown最终也会转为css + html样式。
其实对比浏览器中博客页面和手机app中展示的博客页面,你就会发现手机端支持很弱,根本没有支持主题,甚至还不如使用WebView的效果,所以,提高Html富文本能力也是很重要的。
本篇将通过自定义解析器的方式,实现一个Html标签和CSS可扩展、增强型的引擎。使得TextView支持更多Html和CSS解析和渲染的能力,实现TextView支持更多的富文本效果。
思路
- 方案1: 自定义一套 HTML 解析器,其实很简单,复制一份 android.text.Html,替换其中 SDK 隐藏的 XmlReader 即可
- 方案2:移花接木,通过 Html.TagHandler 夺取解析流程处理器,然后获得拦截解析 html标签 的能力,在拦截到标签之后自行解析。
这两种方案实质上都是可行的,第一种的话要实现自己的 SaxParser 解析,但工作量不小,因此这里我们主要提供方案二的实现方式,同时也能和原有的逻辑相互切换。
最终方案:移花接木
之所以可以移花接木,是因为 TagHandler 会被作为 Html 中标签解析的最后一个流程语句,当遇到自定义的或者 Html 类无法解析的标签,标签调用 TagHandler 的 handleTag 方法会被回调,同时可以获得 TagName,Editable,XmlReader,然后我们便可移花接木。
- 为什么可以移花接木?
- 答案: 在android.text.html类中,只有无法解析的标签才走TagHandler逻辑,因此我们给的起始标签必须不让他解析,下面过程中你就能体会到。
我们移花接木的核心入口是TagHandler,如果TagHandler#handleTag的第一个参数是true,表示开始解析任意标签,false为结束解析任意标签,当然,这里的开始是对所有标签都有效。
1 | java复制代码public static interface TagHandler { |
我们紧接着封装一下解析流程
1 | java复制代码@Override |
我们前面说过,移花接木必须是html的标签无法被解析,下面是源码
android.text.HtmlToSpannedConverter#handleStartTag
1 | java复制代码 |
恰好html标签也无法解析,因此我这里使用html,当然你也可以随便定义,比如myhtml等,都行。
另一方面,在这里我们知道,html是树状结构,因此在树的遍历过程中什么div、section、span、body、head都会走这样的逻辑,但是平时我们使用的Html.fromHtml()的时候,一般不会加上标签在文本开始和结尾处,基于这个习惯,为了方便切换系统定义的渲染方式,我们这里加上html标签
1 | java复制代码private final String H5_TAG = "html"; //自定义标签,该标签无法在原Html类中解析 |
当前仅当,解析到html的时候进行获取解析流程处理器,那什么是解析流程控制器呢?其实主要是4个工具
xmlReader和ContentHandler,当然同时我们也要获取,
但我们添加计数,这个原因主要是防止html出现多层嵌套的问题,导致提前归还解析器控制器
1 | html复制代码<html><section> <html>第二层</html> </section></html> |
核心点,下面是的夺取解析器处理器核心逻辑
1 | java复制代码private void startHandleTag( String tag, Editable output, XMLReader xmlReader) { |
接手控制器之后,我们当然是需要解析的,但是解析需要我们坚挺ContentHandler,具体实现如下
首先对标签进行管理
1 | js复制代码//自定义解析器集合 |
进行拦截解析
1 | java复制代码@Override |
支持自定义标签
其实支持html样式最好还是对标签做处理,单纯的修改css还不如继承父类,好处是有些css样式是可以共用的,不过前提是。
但是在实现代码前,最好研究下Html对标签的标记和提取方法,方便我们后续扩展,下面方法参考android.text.Html类实现。
什么是标记?
在我们创建SpannbleString的时候,我们会对Text段加一些标记,当然标记是可以随便定义的,即便你把它标记成String类型或者Activity类型也是可以的,重要是在渲染逻辑中提取出标记
怎么渲染
这个得研究SpannbleString或者 android.text.Html类,主要是将标记转为TextView能渲染的各种Span,如BackgroundColorSpan和ForegroundSpan等。
1 | java复制代码 //开始解析,主要负责css参数解析和标签标记 |
下面是Html标签的基类,继承该类即可实现你自己的标签和css解析逻辑
1 | java复制代码public abstract class HtmlTag { |
下面我们以实现
定义CSS标记
定义CSS标记是为了记录标签中css 的某一类样式,比如Font和字体相关,background的和背景相关。此类标记是标签解析开始的时候进行标记。
1 | java复制代码 public static class Font{ //定义标记 |
定义Android Span标记
通过上面的css属性标记,仅仅是知道有哪些CSS属性在标签中,但是如何渲染标记呢?这就得依赖TextView中的各种Span标签了。
当然Span有很多种,我们可以选择系统中的,也可以自己定义,我这里为了让FontSpan更强大,自定义了一个新的。注意,这个Span是Spannable中的StyleSpan,和Html Span标签不是同一个概念。
1 | java复制代码public class TextFontSpan extends AbsoluteSizeSpan { |
完整的Html Section标签逻辑
下面实现对Html中的Section标签扩展,使其支持sp、font-size、background-color等css属性
1 | java复制代码public class SectionTag extends HtmlTag { |
用法
替换拦截标签,下面我们替换默认的section标签逻辑,当然你也可以注册成其他的标签,如 field、custom等
1 | java复制代码 |
然后写一段html,输入进去即可
1 | java复制代码 |
或者
1 | java复制代码 |
注意: 标签必须加到要解析的文本段,否则 Android 系统仍然会走 Html 的解析流程,因为我们前面使用的这个来拦截解析器控制器的。
上面的返回结构是Spanned,实际上是配置了各种Span的描述文本,如TextSpan、ForegroundSpan,这意味着,如果我们要扩充css样式,除了标签自定义之外,Span也可能需要自定义。
1 | java复制代码final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler); |
总结
自定义Html标签,使得TextView具备更多更强的html解析能力,其次也能自定义标签,并且实现更多css属性样式,整个过程看似复杂,实际上了解了xml或者html解析过程,你就会对控制流更加熟悉。 另一个知识点是Android Span标记,我们可以注意到,整个过程打了2次标记,第一次是普通css标记,负责记录css属性值,第二次打上Android Span标记,用于TextView渲染逻辑。
当然,我们篇头说过,自行拷贝一份android.text.Html的代码也是阔以的,有些类需要自己找,因为framework的类有些我们无法引用到。
源码
本篇已开源,从下面开源地址获取接口。
AndroidHtmlTag
本文转载自: 掘金