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; //防止自定义的相互嵌套的情况 如: //设置标签计数器,防止自定义标签嵌套自定义标签 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,"无法解析的标签"+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 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的解析流程。
更多相关文章
- 安全新手入坑——HTML标签
- Android(安卓)UI性能优化(一)
- android studio使用fragment标签出错:E/AndroidRuntime: FATAL EX
- Android基于Pull方式解析xml的方法详解
- Android中常见的热门标签的流式布局的实现
- 移动端关于video标签视频全屏播放的兼容适配问题
- Android(安卓)中性能优化之布局优化
- Android中Activity和task,活动亲和力,启动模式,活动状态以及生命周
- 【攻克Android(安卓)(37):XML解析之二】SAX方式解析XML