如何扩展Android富文本之Html标签

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/dbs1215/article/details/102765919

前言

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

富文本

在Android设置富文本一般如下

String txt = "<strong>Hello World</strong>";
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.java
public 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接口有那些方法
在这里插入图片描述
重点关注下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 </br> for each <br>
            // 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方法就是解析富文本中的各种类型标签,从代码看支持有

p
ul
li
div
span
strong
b
em
cite
dfn
i
big
small
font
blockquote
tt
a
u
del
s
strike
sup
sub
img

真正解析标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>

猜你喜欢

转载自blog.csdn.net/dbs1215/article/details/102765919
今日推荐