Android中TextView能夠實現簡單的HTML解析,將Html文本封裝爲Spannable數據實現圖文混排等富文本效果,可是一樣問題不少。css
一、SDK中提供的解析能力不夠強,提供的樣式支持不足,對於css屬性的解析很弱。html
二、不支持多個css樣式同時解析。java
三、SDK中提供的Html.TagHandler沒法獲取到標籤屬性。android
四、可擴展性不夠強,沒法自定義解析器。git
方案1: 自定義一套HTML解析器,其實很簡單,複製一份android.text.Html,替換其中SDK隱藏的XmlReader便可app
方案2:移花接木,經過Html.TagHandler奪取解析流程控制權,而後得到攔截解析tag的能力。ide
這兩種方案實質上都是可行的,第一種的話要實現本身的SaxParse解析,但工做量不小,所以這裏咱們主要提供方案二的實現方式。工具
之因此能夠移花接木,是由於TagHandler會被做爲Html中標籤解析的最後一個流程語句,當遇到自定義的或者Html類沒法解析的標籤,標籤調用TagHandler的handleTag方法會被回調,同時能夠得到TagName,Editable,XmlReader,而後咱們即可移花接木。ui
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 com.example.myapplication.tags.HtmlTag; 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就實現了,接下來實現本身的解析器,爲了更好的約束定義規則,咱們這裏實現一個抽象類,並提供一些解析工具。this
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) { 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的操做,接下來看一個案例。
public class SpanTag extends HtmlTag{ private static Pattern sTextDecorationPattern; private static Pattern sBackgroundColorPattern; private static Pattern sForegroundColorPattern; 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) { final String[] keyValueSet = style.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); return cssVal; } public static class Font{ //定義標記 int textSize; public Font( int textSize) { this.textSize = textSize; } } 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; int textSize = -1; String textColorPattern = getTextColorPattern(style); if (!TextUtils.isEmpty(textColorPattern)) { int c = getHtmlColor(textColorPattern); c = c | 0xFF000000; start(text,new ForegroundColorSpan(c)); } String fontSizePattern = getTextFontSizePattern(style); if(!TextUtils.isEmpty(fontSizePattern)){ textSize = getHtmlSize(fontSizePattern); } if(textSize!=-1){ start(text,new Font(textSize)); //注意,第二個參數能夠爲任意Object類型,這裏起到標記的做用 } String backgroundColorPattern = getBackgroundColorPattern(style); if (!TextUtils.isEmpty(backgroundColorPattern)) { int c = getHtmlColor(backgroundColorPattern); c = c | 0xFF000000; start(text,new Background(c)); } } @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,Font.class,new ForegroundColorSpan(fc.getForegroundColor())); } Font f = getLast(text, Font.class); if (f != null) { end(text,Font.class,new TextFontSpan(f.textSize)); //使用自定義的 } } 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 { private int color; public TextFontSpan(int size) { this(size,false); //這裏咱們以px做爲單位,方便統一調用 } /** * 保持構造方法沒法被外部調用 * @param size * @param dip */ protected TextFontSpan(int size, boolean dip) { super(size, dip); } public TextFontSpan(Parcel src) { super(src); } }
使用方法:
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的解析流程。