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的操作,接下来看一个案例。
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的解析流程。