前语

Android 自界说 html  css标签解析器
要完成上面效果怎样做?

Android 中 TextView 能够完成简略的 HTML 解析,将 Html 文本封装为 Spannable 数据完成图文混排等富文本效果,可是相同问题许多。

  • 1、Android体系中供给的解析才能不够强,供给的CSS款式支撑缺乏,对于css 特点的解析和支撑很弱
  • 2、不支撑多个多种css 款式一起解析
  • 3、SDK 中供给的 Html.TagHandler 无法获取到标签特点
  • 4、无法支撑自界说Html 标签
  • 5、无法支撑自界说CSS特点

基于以上缺陷,假如咱们想在TextView中支撑更丰富的款式,相对来说还不如用SpannableString便利,可是作为Html,他的通用性是现在来说比较高的,就算markdown终究也会转为css + html款式。其实比照浏览器中博客页面和手机app中展示的博客页面,你就会发现手机端支撑很弱,根本没有支撑主题,甚至还不如运用WebView的效果。

思路

  • 计划1: 自界说一套 HTML 解析器,其实很简略,复制一份 android.text.Html,替换其中 SDK 隐藏的 XmlReader 即可
  • 计划2:偷梁换柱,通过 Html.TagHandler 攫取解析流程处理器,然后取得阻拦解析 html标签 的才能,在阻拦到标签之后自行解析。
    这两种计划实质上都是可行的,第一种的话要完成自己的 SaxParser 解析,但工作量不小,因而这儿咱们首要供给计划二的完成办法,一起也能和原有的逻辑彼此切换。

本篇内容其实发表于2019年3月,可是展现量一向不高且之前的博客网站隔三差五出幺蛾子,刚好想换一个博客渠道,趁此搬过来。另外强调时刻还有个原因是因为有人采用了相同的办法,连计数、tag切换逻辑都相同,但我比他更早,以免到时分引起争议。

终究计划:偷梁换柱

之所以能够偷梁换柱,是因为 TagHandler 会被作为 Html 中标签解析的最终一个流程句子,当遇到自界说的或许 Html 类无法解析的标签,标签调用 TagHandler 的 handleTag 办法会被回调,一起能够取得 TagName,Editable,XmlReader,然后咱们便可偷梁换柱。

  • 为什么能够偷梁换柱?
  • 答案: 在android.text.html类中,只要无法解析的标签才走TagHandler逻辑,因而咱们给的起始标签有必要不让他解析,下面过程中你就能体会到。

咱们偷梁换柱的中心进口是TagHandler,假如TagHandler#handleTag的第一个参数是true,表明开端解析恣意标签,false为完毕解析恣意标签,当然,这儿的开端是对所有标签都有用。

public static interface TagHandler {
    public void handleTag(boolean opening, String tag,
                             Editable output, XMLReader xmlReader);
}

咱们紧接着封装一下

@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { 
    if(opening){
      startHandleTag(tag,output,xmlReader); 
    }else{              
      endHandleTag(tag,output,xmlReader);
    } 
}

咱们前面说过,偷梁换柱有必要是html的标签无法被解析

handleStartTag()

刚好html标签也无法解析,因而我这儿运用html,当然也有人是这么做的。

另一方面,在这儿咱们知道,html是树状结构,因而在树的遍历过程中什么div、span、body、head都会走这样的逻辑,可是平时咱们运用的Html.fromHtml()的时分,一般不会加上<html>标签在文本开端和结尾处,基于这个习气,为了便利切换体系界说的烘托办法,咱们这儿加上html标签

private final String H5_TAG = "html"; //自界说标签,该标签无法在原Html类中解析

当前仅当,解析到html的时分进行获取解析流程处理器,那什么是解析流程控制器呢?其实首要是4个工具 xmlReader和ContentHandler,当然一起咱们也要获取,

但咱们增加计数,这个原因首要是避免html呈现多层嵌套的问题,导致提早归还解析器控制器

<html><span> <html>第二层</html> </span></html>

中心点,下面是的攫取解析器处理器中心逻辑

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,详细完成如下 首要对标签进行管理

//自界说解析器调集 
private final Map<String,HtmlTag> tagHandlerMap;

进行阻拦解析

@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
    if (localName.equalsIgnoreCase(H5_TAG)){
        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)){
        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等。

      //开端解析,首要担任css参数解析和标签符号
    public abstract void startHandleTag(Editable text, Attributes attributes); 
     //完毕解析 担任烘托
    public abstract void endHandleTag(Editable text);  //完毕解析

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

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());
    }
    @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);  //完毕解析
}

下面咱们以完成<span>标签为例,这样咱们就重新界说了<span>标签,当然姓名不重要,重要的是你能够随便写,如标签,BaobaoSpan

首要界说符号

  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;
        }
    }

界说Span

当然Span有许多种,咱们能够选择体系中的,也能够自己界说,我这儿为了让FontSpan更强到,自界说了一个新的

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);
        }
    }
}

完整的span标签逻辑

public class SpanTag  extends HtmlTag {
    public SpanTag(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了
            }
        }
    }
}

用法

替换阻拦标签,下面咱们替换默许的span标签逻辑,当然你也能够注册成其他的标签,如 field


HtmlTagHandler htmlTagHandler = new HtmlTagHandler();
htmlTagHandler.registerTag("span",new SpanTag(targetFragment.getContext()));
htmlTagHandler.registerTag("filed",new SpanTag(targetFragment.getContext()));

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


String source = "<html>今日<span>星期三</span>,<span>可是我还要加班</span><html>";
final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler);
textView.setText(spanned );

留意:<html> 标签有必要加到要解析的文本段,不然 Android 体系仍然会走 Html 的解析流程。

总结

自界说Html标签,使得TextView具备更多更强的html解析才能,整个过程看似复杂,实际上了解了xml或许html解析过程,你就会对控制流愈加熟悉。

源码

本篇不供给源码,因为已开源,从下面开源地址获取接口gitee.com/smartian_gi…