Android解析自定义标签

一、需求简介

Android中TextView可以实现简单的HTML解析,将Html文本封装为Spannable数据实现图文混排等富文本效果,但是同样问题很多。

1、SDK中提供的解析能力不够强,提供的样式支持不足,对于css属性的解析很弱。

2、不支持多个css样式同时解析。

3、SDK中提供的Html.TagHandler无法获取到标签属性。

4、可扩展性不够强,无法自定义解析器。

 

二、解决方案

方案1: 自定义一套HTML解析器,其实很简单,复制一份android.text.Html,替换其中SDK隐藏的XmlReader即可

方案2:移花接木,通过Html.TagHandler夺取解析流程控制权,然后获得拦截解析tag的能力。

 

这两种方案实质上都是可行的,第一种的话要实现自己的SaxParse解析,但工作量不小,因此这里我们主要提供方案二的实现方式。

 

三、移花接木

之所以可以移花接木,是因为TagHandler会被作为Html中标签解析的最后一个流程语句,当遇到自定义的或者Html类无法解析的标签,标签调用TagHandler的handleTag方法会被回调,同时可以获得TagName,Editable,XmlReader,然后我们便可移花接木。

 

package com.example.myapplication;

import android.graphics.drawable.Drawable;
import android.support.v4.util.ArrayMap;
import android.text.Editable;
import android.text.Html;
import android.util.Log;


import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class HtmlTagHandler implements Html.TagHandler,Html.ImageGetter, ContentHandler {

    private static final String LOG_TAG  =  "HtmlTagHandler";

    private final String  H5_TAG = "html";  //自定义标签,该标签无法在原Html类中解析
    private volatile ContentHandler orginalContentHandler;
    private int count = 0;  //防止自定义的相互嵌套的情况 如:<html><html></html></html>
    //设置标签计数器,防止自定义标签嵌套自定义标签
    private XMLReader originalXmlReader;
    private Editable originlaEditableText;  //该对象是SpannableStringBuilder
    private List<String> orginalTags = null;

   //自定义解析器集合
    private final Map<String,HtmlTag> tagHandlerMap;


    public HtmlTagHandler( ) {
        String orginalContentHandlerTag = "br|p|ul|li|div|span|strong|b|em|cite|dnf|i|big|small|font|blockquote|tt|a|u|del|s|strike|sup|sub|h1|h2|h3|h4|h5|h6|img";  
//原android.text.Html类中可以解析的标签
        orginalTags = Arrays.asList(orginalContentHandlerTag.split("|"));
        tagHandlerMap = new ArrayMap<>();
    }
    //注册解析器
    public void registerTag(String tagName,HtmlTag tagHandler){
        tagHandlerMap.put(tagName,tagHandler);
    }

    public HtmlTag unregisterTag(String tagName){
        return tagHandlerMap.remove(tagName);
    }

    @Override
    public Drawable getDrawable(String source) {
        return null;
    }
   //处理原Html中无法识别的标签
    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
        if(opening){
            startHandleTag(tag,output,xmlReader);
        }else{
            endHandleTag(tag,output,xmlReader);
        }

    }
    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;
              //还原控制权
            }
        }

    }
    @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+">");
        }
    }
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        orginalContentHandler.characters(ch,start,length); 
    }

    @Override
    public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
        orginalContentHandler.ignorableWhitespace(ch,start,length);
    }

    @Override
    public void processingInstruction(String target, String data) throws SAXException {
        orginalContentHandler.processingInstruction(target,data);
    }

    @Override
    public void skippedEntity(String name) throws SAXException {
        orginalContentHandler.skippedEntity(name);
    }


    @Override
    public void setDocumentLocator(Locator locator) {
        orginalContentHandler.setDocumentLocator(locator);
    }

    @Override
    public void startDocument() throws SAXException {
        orginalContentHandler.startDocument();
    }

    @Override
    public void endDocument() throws SAXException {
        orginalContentHandler.endDocument();
    }

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        orginalContentHandler.startPrefixMapping(prefix,uri);
    }

    @Override
    public void endPrefixMapping(String prefix) throws SAXException {
        orginalContentHandler.endPrefixMapping(prefix);
    }



}

 

以上TagHandler就实现了,接下来实现自己的解析器,为了更好的约束定义规则,我们这里实现一个抽象类,并提供一些解析工具。


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];
        }
    }
    public abstract void startHandleTag(Editable text, Attributes attributes);  //开始解析
    public abstract void endHandleTag(Editable text);  //结束解析


}

实际上,到这里我们的任务已经完成了,按照规则实现解析即可。startHandleTag和endHandleTag因为参数Editable本质上就是SpannableStringBuilder类,同时提供了attributes,接下来的工作无非就是Editable.setSpan的操作,接下来看一个案例。

 

四、案例:改写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了
            }
        }
    }
}
 

 

关于TextFont实现很简单,代码如下

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

 

使用方法:

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

String source = "<html>今天<span style='color:#FFE31335;font-size:16sp;background-color:white;'>星期三</span>,<span style='color:#fff;font-size:14sp;background-color:red;'>但是我还要加班</span><html>";


final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler);
textView.setText(spanned );

注意: <html>标签必须加到要解析的文本段,否则Android系统仍然会走Html的解析流程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值