一、需求简介

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;  //防止自定义的相互嵌套的情况 如:    //设置标签计数器,防止自定义标签嵌套自定义标签    private XMLReader originalXmlReader;    private Editable originlaEditableText;  //该对象是SpannableStringBuilder    private List orginalTags = null;   //自定义解析器集合    private final Map 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,"无法解析的标签");        }    }    @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 sColorNameMap;    static {        sColorNameMap = new ArrayMap();        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 T getLast(Spanned text, Class 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

 

关于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 = "今天星期三但是我还要加班";final Spanned spanned = Html.fromHtml(source, htmlTagHandler, htmlTagHandler);textView.setText(spanned );

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

更多相关文章

  1. 安全新手入坑——HTML标签
  2. Android(安卓)UI性能优化(一)
  3. android studio使用fragment标签出错:E/AndroidRuntime: FATAL EX
  4. Android基于Pull方式解析xml的方法详解
  5. Android中常见的热门标签的流式布局的实现
  6. 移动端关于video标签视频全屏播放的兼容适配问题
  7. Android(安卓)中性能优化之布局优化
  8. Android中Activity和task,活动亲和力,启动模式,活动状态以及生命周
  9. 【攻克Android(安卓)(37):XML解析之二】SAX方式解析XML

随机推荐

  1. Android软键盘-弹起时布局向上拉-多表单
  2. 关于 Android(安卓)程序员最近的状况
  3. Android的背景
  4. Android实现简易版弹钢琴效果
  5. 如何保证Android设备的安全性
  6. Android(安卓)Studio(4)---开发人员工作
  7. android 流量统计实现思路
  8. android framework 启动流程
  9. Android(安卓)Studio检测不到新版本问题
  10. Android(安卓)webview