游戏 SDK 需要接入一个类似公告板的需求,大概样子长这样:
可以看到,中间的内容字体是有不同样式的,之前『Android 改变 TextView 内局部样式』也介绍过如何实现类似的功能。这里由于内容本身也是从服务端获取的,所以服务端生成 HTML 格式文本,客户端再使用 Html.fromHtml()
来解析会比较合适。
不过不知道是服务端富文本插件原因还是其他什么原因,生成过来的富文本格式是这样的:
点击<span style="color: rgb(0, 255, 0)">【大厅】</span>-左下角...
但是实际上这个 RGB 颜色属性是不生效的,除非指定为具体的颜色名称:
点击<span style="color: green">【大厅】</span>-左下角...
或者使用十六进制格式:
点击<span style="color: #00FF00">【大厅】</span>-左下角...
这里主要是用到 <span>
标签,所以进到对应的解析源码查看:
class HtmlToSpannedConverter implements ContentHandler {
...
private void handleStartTag(String tag, Attributes attributes) {
if (...) {
...
} else if (tag.equalsIgnoreCase("span")) {
startCssStyle(mSpannableStringBuilder, attributes);
} else if (...) {
...
}
}
private void startCssStyle(Editable text, Attributes attributes) {
String style = attributes.getValue("", "style");
if (style != null) {
Matcher m = getForegroundColorPattern().matcher(style);
if (m.find()) {
int c = getHtmlColor(m.group(1));
if (c != -1) {
start(text, new Foreground(c | 0xFF000000));
}
}
...
}
}
}
HtmlToSpannedConverter
不是 Html
的内部类,但却写在了 Html.java
文件中,它在 Html.fromHtml()
方法内部被创建。
HTML 的解析对大家来说都没什么难度,跟普通的 XML 解析相似,无非就是先后解析出节点的标签、属性和值,所以我们从标签的解析着手。
首先进入 <span>
标签的分支,判断是否有 style
属性,有的话执行下面的解析。
getForegroundColorPattern()
就是匹配的颜色:
class HtmlToSpannedConverter implements ContentHandler {
...
private static Pattern getForegroundColorPattern() {
if (sForegroundColorPattern == null) {
sForegroundColorPattern = Pattern.compile("(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
}
return sForegroundColorPattern;
}
}
如果匹配上了,就进入 getHtmlColor()
方法:
class HtmlToSpannedConverter implements ContentHandler {
...
private int getHtmlColor(String color) {
...
// If |color| is the name of a color, pass it to Color to convert it. Otherwise,
// it may start with "#", "0", "0x", "+", or a digit. All of these cases are
// handled below by XmlUtils. (Note that parseColor accepts colors starting
// with "#", but it treats them differently from XmlUtils.)
if (Character.isLetter(color.charAt(0))) {
try {
return Color.parseColor(color);
} catch (IllegalArgumentException e) {
return -1;
}
}
try {
return XmlUtils.convertValueToInt(color, -1);
} catch (NumberFormatException nfe) {
return -1;
}
}
}
这里的注释十分清晰,如果传进来的参数是一个颜色名称,那么它将交给 Color
类进行转换;否则,它大概率会以 #
、0
、0x
、+
或数字开头,交给 XmlUtils
处理。
它的判断条件十分简单粗暴,传进来的参数是一个字符串,假如其第一个字符是字母的话,它就将其作为颜色名称来转换,比如 green
就符合条件。
也正是因为这个原因,我们返回的 RGB 色值是 rgb(0, 255, 0)
,第一个字符也是字母,被扯进这个判断中去了。
再到 Color
类看看接下去的步骤:
public class Color {
...
@ColorInt
public static int parseColor(@Size(min=1) String colorString) {
if (colorString.charAt(0) == '#') {
...
} else {
Integer color = sColorNameMap.get(colorString.toLowerCase(Locale.ROOT));
if (color != null) {
return color;
}
}
throw new IllegalArgumentException("Unknown color");
}
}
先判断传入字符串的首个字符是否为 #
,由于上一步已经做了过滤,所以这一步必然是 false
,走下面的 else
分支。也就是从 sColorNameMap
这个 Map
里面根据颜色名称查找对应的色值,接下来看看系统默认定义了哪些色值:
public class Color {
...
private static final HashMap<String, Integer> sColorNameMap;
static {
sColorNameMap = new HashMap<>();
sColorNameMap.put("black", BLACK);
sColorNameMap.put("darkgray", DKGRAY);
sColorNameMap.put("gray", GRAY);
sColorNameMap.put("lightgray", LTGRAY);
sColorNameMap.put("white", WHITE);
sColorNameMap.put("red", RED);
sColorNameMap.put("green", GREEN);
sColorNameMap.put("blue", BLUE);
sColorNameMap.put("yellow", YELLOW);
sColorNameMap.put("cyan", CYAN);
sColorNameMap.put("magenta", MAGENTA);
sColorNameMap.put("aqua", 0xFF00FFFF);
sColorNameMap.put("fuchsia", 0xFFFF00FF);
sColorNameMap.put("darkgrey", DKGRAY);
sColorNameMap.put("grey", GRAY);
sColorNameMap.put("lightgrey", 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);
}
}
很明显,不会有我们传进来的 rgb(0, 255, 0)
,所以会返回 null
,同时抛出异常。
这就是为什么 Html.fromHtml()
不能解析 RBG 格式颜色的原因。