游戏 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 类进行转换;否则,它大概率会以 #00x+ 或数字开头,交给 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 格式颜色的原因。