前言

大家都知道Android 富文本其实就是HTML标签那些东西,但Android本身对其支持有限,今天就说说如何对其进行扩展

富文本

在Android设置富文本一般如下

String txt = "Hello World";textView.setText(HtmlCompat.fromHtml(txt,HtmlCompat.FROM_HTML_MODE_LEGACY));

这样就可以达到加粗的效果;如果要调整字体大小以及颜色呢?有人说很简单把富文本修改成

<span style='font-size:11px;color:#FF1A1A'>Hello World</span>

其实Android中的富文本中span标签中支持的属性有限,运行后你会发现上面写法其实并不生效,那有没办法让其生效呢? 答案是可以的。

我们先从源码角度来大体梳理下fromHtml的执行流程;

fromHtml流程

Html.java

//Html.javapublic static Spanned fromHtml(String source, int flags) {        return fromHtml(source, flags, null, null);    }public static Spanned fromHtml(String source, int flags, android.text.Html.ImageGetter imageGetter,                                   android.text.Html.TagHandler tagHandler) {        //1、创建解析器                                   Parser parser = new Parser();        try {            parser.setProperty(Parser.schemaProperty, HtmlParser.schema);        } catch (org.xml.sax.SAXNotRecognizedException e) {           ...        }//2、构建一个转换器,将html格式转化为原生的Spanned        HtmlToSpannedConverter converter =                new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);        return converter.convert();    }

从代码可以看出非常简单,其实就是将Html的格式转化为Android可以认识的Spanned对象,这样就达到了Android支持富文本的效果了,这里面核心类就是HtmlToSpannedConverter

先看convert方法

public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter,            Html.TagHandler tagHandler, Parser parser, int flags) {        mSource = source;        mSpannableStringBuilder = new SpannableStringBuilder();        mImageGetter = imageGetter;        mTagHandler = tagHandler;        mReader = parser;        mFlags = flags;    }    public Spanned convert() {//1、mReader就是上面的解析器Parser,并绑定了当前对象        mReader.setContentHandler(this);        try {        //2、解析富文本            mReader.parse(new InputSource(new StringReader(mSource)));        } catch (IOException e) {            ...        }        ...        //3、返回了构造器中创建的成员变量        return mSpannableStringBuilder;    }

我们来看下ContentHandler接口有那些方法
如何扩展Android富文本之Html标签_第1张图片
重点关注下startElement方法,从字面意思上我们可以猜测出它是负责标签元素的解析处理的,而Parser.parse方法最终会调用到HtmlToSpannedConverter.startElement方法,

public void startElement(String uri, String localName, String qName, Attributes attributes)            throws SAXException {        handleStartTag(localName, attributes);    }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 
for each
// so we can safely emit the linebreaks when we handle the close tag. } else if (tag.equalsIgnoreCase("p")) { startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph()); startCssStyle(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("span")) { startCssStyle(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("strong")) { start(mSpannableStringBuilder, new Bold()); } ... else if (mTagHandler != null) { mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); } }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)); } } m = getBackgroundColorPattern().matcher(style); if (m.find()) { int c = getHtmlColor(m.group(1)); if (c != -1) { start(text, new Background(c | 0xFF000000)); } } m = getTextDecorationPattern().matcher(style); if (m.find()) { String textDecoration = m.group(1); if (textDecoration.equalsIgnoreCase("line-through")) { start(text, new Strikethrough()); } } } }private static void start(Editable text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); }

从上面代码看出,handleStartTag方法就是解析富文本中的各种类型标签,从代码看支持有

pullidivspanstrongbemcitedfnibigsmallfontblockquotettaudelsstrikesupsubimg

真正解析标span标签的其实就是startCssStyle方法,从代码看该方法支持的属性有限,所以扩展span标签中属性其实一大部分就是考虑如何改写startCssStyle方法,其实类中除了startXxx方法还有endXxx方法,endCssStyle方法就是将startXxx方法中解析出的数据转变为原生的可识别数据并设置到mSpannableStringBuilder中

private static void endCssStyle(Editable text) {        ...        Foreground f = getLast(text, Foreground.class);        if (f != null) {            setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));        }    }private static void setSpanFromMark(Spannable text, Object mark, Object... spans) {        int where = text.getSpanStart(mark);        text.removeSpan(mark);        int len = text.length();        if (where != len) {            for (Object span : spans) {                text.setSpan(span, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);            }        }    }

所以扩展span标签的思路很明确了,第一步在startCssStyle方法中解析出style标签中的属性集合,第二部在endCssStyle中对上一步解析的数据进行转化;

扩展

1. 类拷贝

因为startCssStyle方法都是私有我们无法复写,所以我们可以考虑把新建二个类来替代HtmlCompat、Html;先把Android原生的二个类拷贝到自己新建的二个类中,最后你会发现编译会失败,需要稍微调整下源码

调整一

Html.java

Application application = ActivityThread.currentApplication();

可以把它替换成

public static Application getCurrentApplication() {        try {            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");            Method method = activityThreadClass.getMethod("currentApplication");            return (Application) method.invoke(null, (Object[]) null);        } catch (Exception e) {            e.printStackTrace();        }        return null;    }

调整二

private static void startImg(Editable text, Attributes attributes, Html.ImageGetter img) {        String src = attributes.getValue("", "src");        Drawable d = null;        if (img != null) {            d = img.getDrawable(src);        }        if (d == null) {            //d = Resources.getSystem().getDrawable(com.android.internal.R.drawable.unknown_image);            //替换成下面二句            int resId = Resources.getSystem().getIdentifier("unknown_image", "drawable", "android");            d = Resources.getSystem().getDrawable(resId);                        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());        }        int len = text.length();        text.append("\uFFFC");        text.setSpan(new ImageSpan(d, src), len, text.length(),                     Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);    }

调整三

private int getHtmlColor(String color) {        if ((mFlags & android.text.Html.FROM_HTML_OPTION_USE_CSS_COLORS)                == android.text.Html.FROM_HTML_OPTION_USE_CSS_COLORS) {            Integer i = sColorMap.get(color.toLowerCase(Locale.US));            if (i != null) {                return i;            }        }//        return Color.getHtmlColor(color);//替换下面        try {            return convertValueToInt(color, -1);        } catch (NumberFormatException nfe) {            return -1;        }    }public static final int convertValueToInt(CharSequence charSeq, int defaultValue)    {        if (null == charSeq)            return defaultValue;        String nm = charSeq.toString();        // XXX This code is copied from Integer.decode() so we don't        // have to instantiate an Integer!        int value;        int sign = 1;        int index = 0;        int len = nm.length();        int base = 10;        if ('-' == nm.charAt(0)) {            sign = -1;            index++;        }        if ('0' == nm.charAt(index)) {            //  Quick check for a zero by itself            if (index == (len - 1))                return 0;            char    c = nm.charAt(index + 1);            if ('x' == c || 'X' == c) {                index += 2;                base = 16;            } else {                index++;                base = 8;            }        }        else if ('#' == nm.charAt(index))        {            index++;            base = 16;        }        return Integer.parseInt(nm.substring(index), base) * sign;    }

调整四

Parser类为系统自带的tagsoup库,我们为确保编译成功需在build.gradle文件添加

dependencies {compileOnly 'org.ccil.cowan.tagsoup:tagsoup:1.2.1'}

2. 改写方法

private void startCssStyle(Editable text, Attributes attributes) {        String style = attributes.getValue("", "style");        if (style != null) {            String[] entryArray = style.split(";");            if (entryArray != null) {                for (String entry : entryArray) {                    String[] kv = entry.split(":");                    if (kv == null                            || kv.length < 2                            || TextUtils.isEmpty(kv[0])                            || TextUtils.isEmpty(kv[1])) {                        continue;                    }                    String key = kv[0];                    String value = kv[1];                    /**                     * support font-size                     */                    if ("font-size".equalsIgnoreCase(key)) {                        if (!TextUtils.isEmpty(value)) {                            if (value.endsWith("px")) {                                int size = (int) Float.parseFloat(value.substring(0, value.length() - 2));                                start(text, new Size(size));                            }                        }                    }//support color                    if ("color".equalsIgnoreCase(key)) {                        if (!TextUtils.isEmpty(value)) {                            int c = getHtmlColor(value);                            if (c != -1) {                                start(text, new Foreground(c | 0xFF000000));                            }                        }                    }                }            }        }//Android Origin Code//        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));//                }//            }////            m = getBackgroundColorPattern().matcher(style);//            if (m.find()) {//                int c = getHtmlColor(m.group(1));//                if (c != -1) {//                    start(text, new Background(c | 0xFF000000));//                }//            }////            m = getTextDecorationPattern().matcher(style);//            if (m.find()) {//                String textDecoration = m.group(1);//                if (textDecoration.equalsIgnoreCase("line-through")) {//                    start(text, new Strikethrough());//                }//            }//        }    }private static class Size {        public int mSize;        public Size(int size) {            mSize = size;        }    }
private static void endCssStyle(Editable text) {        Strikethrough s = getLast(text, Strikethrough.class);        if (s != null) {            setSpanFromMark(text, s, new StrikethroughSpan());        }        Background b = getLast(text, Background.class);        if (b != null) {            setSpanFromMark(text, b, new BackgroundColorSpan(b.mBackgroundColor));        }        Foreground f = getLast(text, Foreground.class);        if (f != null) {            setSpanFromMark(text, f, new ForegroundColorSpan(f.mForegroundColor));        }        /**         * support font-size         */        Size size = getLast(text, Size.class);        if (size != null) {            setSpanFromMark(text, size, new AbsoluteSizeSpan(size.mSize, true));        }    }

这样就使Android支持下面富文本样式,当然我们可以参照上述操作可以继续扩展支持其他属性等。。。。

<span style='font-size:11px;color:#FF1A1A'>Hello World</span>

更多相关文章

  1. Android获取WIFI 的ssid 方法适配Android9.0
  2. Android 出现 OutOfMemoryError 的一种解决方法
  3. android keytool 不是内部命令或外部命令在 (win7下不能用的解决
  4. Arcgis android 10.2安装方法
  5. Android studio 打不开官方虚拟机 100%成功解决方法
  6. 输入法软键盘搜索执行两次的解决方法
  7. Android 开发——'Android Pre Compiler'空指针问题的解决方法
  8. Android 自定义TextView 实现文本间距
  9. android 各个span类详解--用于富文本编排 下

随机推荐

  1. SQL Server数据库安装时常见问题解决方案
  2. 编写高质量代码改善C#程序——使用泛型集
  3. 如何统计全天各个时间段产品销量情况(sqls
  4. 大数据量高并发的数据库优化详解
  5. SQL Server安装完成后3个需要立即修改的
  6. MySql查询不区分大小写解决方案(两种)
  7. 数据库设计三大范式简析
  8. 跨数据库实现数据交流
  9. 防御SQL注入的方法总结
  10. 用户 jb51net 登录失败。原因: 该帐户的