Android 实现html css富文本解析引擎 前言 思

前言

企业微信20231220-141336@2x.png

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 解析,但工作量不小,因此这里我们主要提供方案二的实现方式,同时也能和原有的逻辑相互切换。

企业微信20240221-074937@2x.png

最终方案:移花接木

之所以可以移花接木,是因为 TagHandler 会被作为 Html 中标签解析的最后一个流程语句,当遇到自定义的或者 Html 类无法解析的标签,标签调用 TagHandler 的 handleTag 方法会被回调,同时可以获得 TagName,Editable,XmlReader,然后我们便可移花接木。

  • 为什么可以移花接木?
  • 答案: 在android.text.html类中,只有无法解析的标签才走TagHandler逻辑,因此我们给的起始标签必须不让他解析,下面过程中你就能体会到。

我们移花接木的核心入口是TagHandler,如果TagHandler#handleTag的第一个参数是true,表示开始解析任意标签,false为结束解析任意标签,当然,这里的开始是对所有标签都有效。

1
2
3
4
java复制代码public static interface TagHandler {
public void handleTag(boolean opening, String tag,
Editable output, XMLReader xmlReader);
}

我们紧接着封装一下解析流程

1
2
3
4
5
6
7
8
9
java复制代码@Override 
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if(opening){
startHandleTag(tag,output,xmlReader);
}else{
endHandleTag(tag,output,xmlReader);
}

}

我们前面说过,移花接木必须是html的标签无法被解析,下面是源码
android.text.HtmlToSpannedConverter#handleStartTag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
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 emit the linebreaks when we handle the close tag.
// 对于类似br这种换行,TagSoup会自动扩充为成对表签 <br> </br>
} else if (tag.equalsIgnoreCase("p")) {
startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());
startCssStyle(mSpannableStringBuilder, attributes);
} else if (tag.equalsIgnoreCase("ul")) {
startBlockElement(mSpannableStringBuilder, attributes, getMarginList());
}
// 省略一些代码
if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
} else if (mTagHandler != null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
}
}

恰好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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码private void startHandleTag( String tag, Editable output, XMLReader xmlReader) {

if (tag.equalsIgnoreCase(H5_TAG)){
if(orginalContentHandler==null) {
orginalContentHandler = xmlReader.getContentHandler();
this.originalXmlReader = xmlReader; //获取XmlReader
this.originalXmlReader.setContentHandler(this);//获取控制权,让本类监听解析流程
this.originlaEditableText = output; //获取到SpannableStringBuilder

}
count++;
}

}

private void endHandleTag( String tag, Editable output, XMLReader xmlReader) {
if(tag.equalsIgnoreCase(tag)){
count--;
if(count==0 ){
this.originalXmlReader.setContentHandler(this.orginalContentHandler);
//将原始的handler交还
this.originalXmlReader = null;
this.originlaEditableText = null;
this.orginalContentHandler = null;
//还原控制权
}
}

}

接手控制器之后,我们当然是需要解析的,但是解析需要我们坚挺ContentHandler,具体实现如下
首先对标签进行管理

1
2
js复制代码//自定义解析器集合 
private final Map<String,HtmlTag> tagHandlerMap;

进行拦截解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {

if (localName.equalsIgnoreCase(H5_TAG)){
//防止 <html>多次内嵌
handleTag(true,localName,this.originlaEditableText,this.originalXmlReader);
}else if(canHandleTag(localName)){ //拦截,判断是否可以解析该标签

final HtmlTag htmlTag = tagHandlerMap.get(localName); //读取自定义解析器开始解析
htmlTag.startHandleTag(this.originlaEditableText,atts);

}else if(orginalTags.contains(localName)){ //无法解析的优先让原Html类解析
this.orginalContentHandler.startElement(uri,localName,qName,atts);
}else{
Log.e(LOG_TAG,"无法解析的标签<"+localName+">");
}

}

private boolean canHandleTag(String tagName) {
if(!tagHandlerMap.containsKey(tagName)){
return false;
}
final HtmlTag htmlTag = tagHandlerMap.get(tagName);
return htmlTag!=null;

}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {

if (localName.equalsIgnoreCase(H5_TAG)){
//防止 <html>多次内嵌
handleTag(false,localName,this.originlaEditableText,this.originalXmlReader);
}else if(canHandleTag(localName)){
final HtmlTag htmlTag = tagHandlerMap.get(localName); //读取自定义解析器结束解析
htmlTag.endHandleTag(this.originlaEditableText);
}else if(orginalTags.contains(localName)){
this.orginalContentHandler.endElement(uri,localName,qName);
}else{
Log.e(LOG_TAG,"无法解析的标签</"+localName+">");
}
}

支持自定义标签

其实支持html样式最好还是对标签做处理,单纯的修改css还不如继承父类,好处是有些css样式是可以共用的,不过前提是。

但是在实现代码前,最好研究下Html对标签的标记和提取方法,方便我们后续扩展,下面方法参考android.text.Html类实现。

什么是标记?

在我们创建SpannbleString的时候,我们会对Text段加一些标记,当然标记是可以随便定义的,即便你把它标记成String类型或者Activity类型也是可以的,重要是在渲染逻辑中提取出标记

怎么渲染

这个得研究SpannbleString或者 android.text.Html类,主要是将标记转为TextView能渲染的各种Span,如BackgroundColorSpan和ForegroundSpan等。

1
2
3
4
java复制代码      //开始解析,主要负责css参数解析和标签标记
public abstract void startHandleTag(Editable text, Attributes attributes);
//结束解析 负责渲染
public abstract void endHandleTag(Editable text); //结束解析

下面是Html标签的基类,继承该类即可实现你自己的标签和css解析逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
java复制代码public abstract class HtmlTag {

private Context context;

public HtmlTag(Context context) {
this.context = context;
}

public Context getContext() {
return context;
}

private static final Map<String, Integer> sColorNameMap;

static {
sColorNameMap = new ArrayMap<String, Integer>();
sColorNameMap.put("black", Color.BLACK);
sColorNameMap.put("darkgray", Color.DKGRAY);
sColorNameMap.put("gray", Color.GRAY);
sColorNameMap.put("lightgray", Color.LTGRAY);
sColorNameMap.put("white", Color.WHITE);
sColorNameMap.put("red", Color.RED);
sColorNameMap.put("green", Color.GREEN);
sColorNameMap.put("blue", Color.BLUE);
sColorNameMap.put("yellow", Color.YELLOW);
sColorNameMap.put("cyan", Color.CYAN);
sColorNameMap.put("magenta", Color.MAGENTA);
sColorNameMap.put("aqua", 0xFF00FFFF);
sColorNameMap.put("fuchsia", 0xFFFF00FF);
sColorNameMap.put("darkgrey", Color.DKGRAY);
sColorNameMap.put("grey", Color.GRAY);
sColorNameMap.put("lightgrey", Color.LTGRAY);
sColorNameMap.put("lime", 0xFF00FF00);
sColorNameMap.put("maroon", 0xFF800000);
sColorNameMap.put("navy", 0xFF000080);
sColorNameMap.put("olive", 0xFF808000);
sColorNameMap.put("purple", 0xFF800080);
sColorNameMap.put("silver", 0xFFC0C0C0);
sColorNameMap.put("teal", 0xFF008080);
sColorNameMap.put("white", Color.WHITE);
sColorNameMap.put("transparent", Color.TRANSPARENT);

}

@ColorInt
public static int getHtmlColor(String colorString){

if(sColorNameMap.containsKey(colorString.toLowerCase())){
Integer colorInt = sColorNameMap.get(colorString);
if(colorInt!=null) return colorInt;
}

return parseHtmlColor(colorString.toLowerCase());
}
//颜色解析器,我们做下扩展,使得其支持argb,还有很多可以自己实现
@ColorInt
public static int parseHtmlColor( String colorString) {

if (colorString.charAt(0) == '#') {
if(colorString.length()==4){
StringBuilder sb = new StringBuilder("#");
for (int i=1;i<colorString.length();i++){
char c = colorString.charAt(i);
sb.append(c).append(c);
}
colorString = sb.toString();
}
long color = Long.parseLong(colorString.substring(1), 16);
if (colorString.length() == 7) {
// Set the alpha value
color |= 0x00000000ff000000;
} else if (colorString.length() == 9) {

int alpha = Integer.parseInt(colorString.substring(1,3),16) ;
int red = Integer.parseInt(colorString.substring(3,5),16);
int green = Integer.parseInt(colorString.substring(5,7),16);
int blue = Integer.parseInt(colorString.substring(7,8),16);
color = Color.argb(alpha,red,green,blue);
}else{
throw new IllegalArgumentException("Unknown color");
}
return (int)color;
}
else if(colorString.startsWith("rgb(") || colorString.startsWith("rgba(") && colorString.endsWith(")"))
{
colorString = colorString.substring(colorString.indexOf("("),colorString.indexOf(")"));
colorString = colorString.replaceAll(" ","");
String[] colorArray = colorString.split(",");
if(colorArray.length==3){
return Color.argb(255,Integer.parseInt(colorArray[0]),Integer.parseInt(colorArray[1]),Integer.parseInt(colorArray[2]));
}
else if (colorArray.length==4){
return Color.argb(Integer.parseInt(colorArray[3]),Integer.parseInt(colorArray[0]),Integer.parseInt(colorArray[1]),Integer.parseInt(colorArray[2]));
}

}
throw new IllegalArgumentException("Unknown color");
}

//负责提取标记
public static <T> T getLast(Spanned text, Class<T> kind) {

T[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
} else {
return objs[objs.length - 1];
}
}
//开始解析,主要负责css参数解析和标签标记
public abstract void startHandleTag(Editable text, Attributes attributes);
//结束解析 负责渲染
public abstract void endHandleTag(Editable text); //结束解析

}

下面我们以实现

标签为例,这样我们就重新定义了
标签,当然名字不重要,重要的是你可以随便写,如标签或者BaobaoSection

定义CSS标记

定义CSS标记是为了记录标签中css 的某一类样式,比如Font和字体相关,background的和背景相关。此类标记是标签解析开始的时候进行标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码  public static class Font{  //定义标记
int textSize;
int textDecordation;
int fontWeidght;

public Font(int textSize,int textDecordation,int fontWeidght) {
this.textSize = textSize;
this.textDecordation = textDecordation;
this.fontWeidght = fontWeidght;
}
}

public static class Background{ //定义标记
int color;
public Background(int color) {
this.color = color;
}
}

定义Android Span标记

通过上面的css属性标记,仅仅是知道有哪些CSS属性在标签中,但是如何渲染标记呢?这就得依赖TextView中的各种Span标签了。

当然Span有很多种,我们可以选择系统中的,也可以自己定义,我这里为了让FontSpan更强大,自定义了一个新的。注意,这个Span是Spannable中的StyleSpan,和Html Span标签不是同一个概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
java复制代码public class TextFontSpan extends AbsoluteSizeSpan {

public static final int FontWidget_NORMAL= 400;
public static final int FontWidget_BOLD = 750;

public static final int TextDecoration_NONE=0;
public static final int TextDecoration_UNDERLINE=1;
public static final int TextDecoration_LINE_THROUGH=2;
public static final int TextDecoration_OVERLINE=3;

private int fontWidget = -1;
private int textDecoration = -1;

private int mSize = -1;

public TextFontSpan(int size ,int textDecoration,int fontWidget) {
this(size,false);
this.mSize = size;
this.fontWidget = fontWidget;
this.textDecoration = textDecoration;
//这里我们以px作为单位,方便统一调用
}

/**
* 保持构造方法无法被外部调用
* @param size
* @param dip
*/
protected TextFontSpan(int size, boolean dip) {
super(size, dip);
}

public TextFontSpan(Parcel src) {
super(src);
fontWidget = src.readInt();
textDecoration = src.readInt();
mSize = src.readInt();
}

@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(fontWidget);
dest.writeInt(textDecoration);
dest.writeInt(mSize);
}

@Override
public void updateDrawState(TextPaint ds) {
if(this.mSize>=0){
super.updateDrawState(ds);
}

if(fontWidget==FontWidget_BOLD) {
ds.setFakeBoldText(true);
}else if(fontWidget==FontWidget_NORMAL){
ds.setFakeBoldText(false);
}
if(textDecoration==TextDecoration_NONE) {
ds.setStrikeThruText(false);
ds.setUnderlineText(false);
}else if(textDecoration==TextDecoration_LINE_THROUGH){
ds.setStrikeThruText(true);
ds.setUnderlineText(false);
}else if(textDecoration==TextDecoration_UNDERLINE){
ds.setStrikeThruText(false);
ds.setUnderlineText(true);
}

}

@Override
public void updateMeasureState(TextPaint ds) {
if(this.mSize>=0){
super.updateMeasureState(ds);
}

if(fontWidget==FontWidget_BOLD) {
ds.setFakeBoldText(true);
}else if(fontWidget==FontWidget_NORMAL){
ds.setFakeBoldText(false);
}

if(textDecoration==TextDecoration_NONE) {
ds.setStrikeThruText(false);
ds.setUnderlineText(false);
}else if(textDecoration==TextDecoration_LINE_THROUGH){
ds.setStrikeThruText(true);
ds.setUnderlineText(false);
}else if(textDecoration==TextDecoration_UNDERLINE){
ds.setStrikeThruText(false);
ds.setUnderlineText(true);
}
}
}

完整的Html Section标签逻辑

下面实现对Html中的Section标签扩展,使其支持sp、font-size、background-color等css属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
java复制代码public class SectionTag extends HtmlTag {


public SectionTag(Context context) {
super(context);
}


private int getHtmlSize(String fontSize) {
fontSize = fontSize.toLowerCase();
if(fontSize.endsWith("px")){
return (int) Double.parseDouble(fontSize.substring(0,fontSize.indexOf("px")));
}else if(fontSize.endsWith("sp") ){
float sp = (float) Double.parseDouble(fontSize.substring(0,fontSize.indexOf("sp")));
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getContext().getResources().getDisplayMetrics());
}else if(TextUtils.isDigitsOnly(fontSize)){ //如果不带单位,默认按照sp处理
float sp = (float) Double.parseDouble(fontSize);
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getContext().getResources().getDisplayMetrics());
}
return -1;
}

private static String getTextColorPattern(String style) {
String cssName = "text-color";
String cssVal = getHtmlCssValue(style, cssName);
if(TextUtils.isEmpty(cssVal)){
cssName = "color";
cssVal = getHtmlCssValue(style, cssName);
}
return cssVal;
}

@Nullable
private static String getHtmlCssValue(String style, String cssName) {
if(TextUtils.isEmpty(style)) return null;
final String[] keyValueSet = style.toLowerCase().split(";");
if(keyValueSet==null) return null;
for (int i=0;i<keyValueSet.length;i++){
final String match = keyValueSet[i].replaceAll(" ","").toLowerCase();
if(match.indexOf(cssName)==0){
final String[] parts = match.split(":");
if(parts==null || parts.length!=2) continue;
return parts[1];
}
}
return null;
}

private static String getBackgroundColorPattern(String style) {
String cssName = "background-color";
String cssVal = getHtmlCssValue(style, cssName);

if(TextUtils.isEmpty(cssVal)){
cssName = "bakground";
cssVal = getHtmlCssValue(style, cssName);
}

return cssVal;
}

private static String getTextFontSizePattern(String style) {
String cssName = "font-size";
String cssVal = getHtmlCssValue(style, cssName);
if(TextUtils.isEmpty(cssVal)){
cssName = "text-size";
cssVal = getHtmlCssValue(style, cssName);
}
return cssVal;
}

private static String getTextDecorationPattern(String style) {
String cssName = "text-decoration";
String cssVal = getHtmlCssValue(style, cssName);
return cssVal;
}
private static String getTextFontPattern(String style) {
String cssName = "font-weight";
String cssVal = getHtmlCssValue(style, cssName);
return cssVal;
}


public static class Font{ //定义标记
int textSize;
int textDecordation;
int fontWeidght;

public Font( int textSize,int textDecordation,int fontWeidght) {
this.textSize = textSize;
this.textDecordation = textDecordation;
this.fontWeidght = fontWeidght;
}
}

public static class Background{ //定义标记
int color;
public Background(int color) {
this.color = color;
}
}

@Override
public void startHandleTag(Editable text, Attributes attributes) {
String style = attributes.getValue("", "style");
if(TextUtils.isEmpty(style)) return;


String textColorPattern = getTextColorPattern(style);
if (!TextUtils.isEmpty(textColorPattern)) {
int c = getHtmlColor(textColorPattern);
c = c | 0xFF000000;
start(text,new ForegroundColorSpan(c));

}

startMarkTextFont(text,style);

String backgroundColorPattern = getBackgroundColorPattern(style);
if (!TextUtils.isEmpty(backgroundColorPattern)) {
int c = getHtmlColor(backgroundColorPattern);
c = c | 0xFF000000;
//注意,第二个参数可以为任意Object类型,这里起到标记的作用
start(text,new Background(c));
}

}

private void startMarkTextFont(Editable text ,String style) {

String fontSize = getTextFontSizePattern(style);
String textDecoration = getTextDecorationPattern(style);
String fontWidget = getTextFontPattern(style);

int textSize = -1;
if(TextUtils.isEmpty(fontSize)){
if(!TextUtils.isEmpty(fontSize)){
textSize = getHtmlSize(fontSize);
}
}
int textDecorationVal = -1;
if(!TextUtils.isEmpty(textDecoration)){
if(textDecoration.equals("underline")) {
textDecorationVal = TextFontSpan.TextDecoration_UNDERLINE;
}else if(textDecoration.equals("line-through")){
textDecorationVal = TextFontSpan.TextDecoration_LINE_THROUGH;
}
else if(textDecoration.equals("overline")){
textDecorationVal = TextFontSpan.TextDecoration_OVERLINE;//暂不支持
} else if(textDecoration.equals("none")){
textDecorationVal = TextFontSpan.TextDecoration_NONE;
}
}
int fontWeidgtVal = -1;
if(!TextUtils.isEmpty(fontWidget)){
if(textDecoration.equals("normal")) {
fontWeidgtVal = TextFontSpan.FontWidget_NORMAL;
}else if(textDecoration.equals("bold")){
fontWeidgtVal = TextFontSpan.FontWidget_BOLD;
}
}

start(text,new Font(textSize,textDecorationVal,fontWeidgtVal));
}

@Override
public void endHandleTag(Editable text){


Background b = getLast(text, Background.class); //读取出最后标记类型
if(b!=null){
end(text,Background.class,new BackgroundColorSpan(b.color)); //设置为Android可以解析的24种ParcelableSpan基本分类,当然也可以自己定义,但需要集成原有的分类
}

final ForegroundColorSpan fc = getLast(text, ForegroundColorSpan.class);
if(fc!=null){
end(text,ForegroundColorSpan.class,new ForegroundColorSpan(fc.getForegroundColor()));
}

Font f = getLast(text, Font.class);
if (f != null) {
end(text,Font.class,new TextFontSpan(f.textSize,f.textDecordation,f.fontWeidght)); //使用自定义的
}
}

private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); //添加标记在最后一位,注意开始位置和结束位置
}

@SuppressWarnings("unchecked")
private static void end(Editable text, Class kind, Object repl) {
Object obj = getLast(text, kind); //读取kind类型
if (obj != null) {
setSpanFromMark(text, obj, repl);
}
}


private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {
int where = text.getSpanStart(mark);
text.removeSpan(mark);
//移除原有标记,因为原有标记不是默认的24种ParcelableSpan子类,因此无法渲染文本
int len = text.length();
if (where != len) {
for (Object span : spans) {
text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //注意:开始位置和结束位置,因为SpannableStringBuilder的append添加字符方法导致len已经大于where了
}
}
}
}

用法

替换拦截标签,下面我们替换默认的section标签逻辑,当然你也可以注册成其他的标签,如 field、custom等

1
2
3
4
5
java复制代码
HtmlTagHandler htmlTagHandler = new HtmlTagHandler();
htmlTagHandler.registerTag("section",new SectionTag(targetFragment.getContext()));
htmlTagHandler.registerTag("custom",new SectionTag(targetFragment.getContext()));
htmlTagHandler.registerTag("span",new SectionTag(targetFragment.getContext()));

然后写一段html,输入进去即可

1
2
3
4
5
6
java复制代码
String source = "<html>今天<section style='color:#FFE31335;font-size:16sp;background-color:white;'>星期三</section>,<section style='color:#fff;font-size:14sp;background-color:red;'>但是我还要加班</section><html>";

final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler);

textView.setText(spanned );

或者

1
2
3
4
5
java复制代码
String source = "<html>今天<span style='color:#FFE31335;font-size:16sp;background-color:white;'>星期三</span>,<custom style='color:#fff;font-size:14sp;background-color:red;'>但是我还要加班</custom><html>";

final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler);
textView.setText(spanned );

注意: 标签必须加到要解析的文本段,否则 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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%