Android富文字Html原始碼解析【安卓巴士博文大賽】
Html能夠透過Html標籤來為文字設定樣式,讓TextView顯示富文字資訊,其只支援部分標籤不是全部,具體支援哪些標籤將分析中揭曉。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView textView = (TextView) findViewById(R.id.tv_html); String htmlString = "顏色
" + "br/>" + "大字型
"+ "小字型
"+ "加粗
"+ "斜體
" + "標題一
" + "標題二
" + "" + "引用" + "塊" + "下劃線
" + "上標正常字型下標
" + "組合樣式字體"; textView.setText(Html.fromHtml(htmlString)); }
由此可以看出Html還是比較強大的一個東西呀!
使用Html.toHtml方法能夠將帶有樣式效果的Spanned文字物件生成對應的Html格式,標籤內的字元會被轉譯成,下面為WebView顯示效果,部分效果與上面TextView顯示的效果有差異,程式碼如下:
webView.loadData(Html.toHtml(Html.fromHtml(htmlString)),"text/html", "utf-8");
顯示效果還是有點差距的,用的是安卓4.0.3的手機系統,所以可能顯示上有點問題,不過應該不影響大家區分。重點畢竟不在這裡,大家繼續往下看原理吧!
原理分析
首先我們先看看html類:
/*** 該類將HTML處理成帶樣式的文字,但不支援所有的HTML標籤
*/
public class Html {
/** * 為標籤提供圖片檢索功能 */public static interface ImageGetter { /** * 當HTML解析器解析到標籤時,source引數為標籤中的src的屬性值, * 返回值必須為Drawable;如果返回null則會使用小方塊來顯示,如前面所見, * 並需要呼叫Drawable.setBounds()方法來設定大小,否則無法顯示圖片。 * @param source: */ public Drawable getDrawable(String source); }/** * HTML標籤解析擴充套件介面 */public static interface TagHandler { /** * 當解析器解析到本身不支援或使用者自定義的標籤時,該方法會被呼叫 * @param opening:標籤是否開啟 * @param tag:標籤名 * @param output:截止到當前標籤,解析到的文字內容 * @param xmlReader:解析器物件 */ public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader); }private Html() { }/** * 返回樣式文字,所有標籤都會顯示為一個小方塊 * 使用TagSoup庫處理HTML * @param source:帶有html標籤字串 */public static Spanned fromHtml(String source) { return fromHtml(source, null, null); }/** * 可傳入ImageGetter來獲取圖片源,TagHandler新增支援其他標籤 */public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { ..... }/** * 將帶樣式文字反向解析成帶Html的字串,注意這個方法並不是還原成fromHtml接收的帶Html標籤文字 */public static String toHtml(Spanned text) { StringBuilder out = new StringBuilder(); withinHtml(out, text); return out.toString(); }/** * 返回轉譯標籤後的字串 */public static String escapeHtml(CharSequence text) { StringBuilder out = new StringBuilder(); withinStyle(out, text, 0, text.length()); return out.toString(); }/** * 懶載入HTML解析器的Holder * a) zygote對其進行預載入 * b) 直到需要的時候才載入 */private static class HtmlParser { private static final HTMLSchema schema = new HTMLSchema(); } 。。。。
fromHtml(String source, ImageGetter imageGetter,TagHandler tagHandler):
Html類主要方法就4個,功能也簡單,生成帶樣式的fromHtml方法最終都是呼叫過載3個引數的方法。
public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) { //初始化解析器 Parser parser = new Parser(); try { //配置解析Html模式 parser.setProperty(Parser.schemaProperty, HtmlParser.schema); } catch (org.xml.sax.SAXNotRecognizedException e) { throw new RuntimeException(e); } catch (org.xml.sax.SAXNotSupportedException e) { throw new RuntimeException(e); } //初始化真正的解析器 HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler,parser); return converter.convert(); }
原始碼中並沒有包含Parser物件,而是必須匯入org.ccil.cowan.tagsoup.Parser,HTML解析器是使用Tagsoup庫來解析HTML標籤,Tagsoup是相容SAX的解析器,我們知道對XML常見的的解析方式還有DOM、Android系統中還使用PULL解析與SAX同樣是基於事件驅動模型,使用tagsoup是因為該庫可以將HTML轉化為XML,我們都知道HTML有時候並不像XML那樣標籤都需要閉合,例如
也是一個有效的標籤,但是在XML中則是不良格式。詳情可見官方網站,但是好像沒有開發文件,這裡就不詳細說明,只關注SAX解析過程。
HtmlToSpannedConverter原理
class HtmlToSpannedConverter implements ContentHandler { private static final float[] HEADER_SIZES = { 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, }; private String mSource; private XMLReader mReader; private SpannableStringBuilder mSpannableStringBuilder; private Html.ImageGetter mImageGetter; private Html.TagHandler mTagHandler; public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser) { mSource = source;//html文字 mSpannableStringBuilder = new SpannableStringBuilder();//用於存放標籤中的字串 mImageGetter = imageGetter;//圖片載入器 mTagHandler = tagHandler;//自定義標籤器 mReader = parser;//解析器 } public Spanned convert() { //設定內容處理器 mReader.setContentHandler(this); try { //開始解析 mReader.parse(new InputSource(new StringReader(mSource))); } catch (IOException e) { // We are reading from a string. There should not be IO problems. throw new RuntimeException(e); } catch (SAXException e) { // TagSoup doesn't throw parse exceptions. throw new RuntimeException(e); } //省略 ... ... return mSpannableStringBuilder; }
透過上面程式碼可以發現,SpannableStringBuilder是用來存放解析html標籤中的字串,類似StringBuilder,但它附帶有樣式的字串。重點關注convert裡面的setContentHandler方法,該方法接收的是ContentHandler介面,使用過SAX解析的讀者應該不陌生,該介面定義了一系列SAX解析事件的方法。
public interface ContentHandler{ //設定文件定位器 public void setDocumentLocator (Locator locator); //文件開始解析事件 public void startDocument () throws SAXException; //文件結束解析事件 public void endDocument() throws SAXException; //解析到名稱空間字首事件 public void startPrefixMapping (String prefix, String uri) throws SAXException; //結束名稱空間事件 public void endPrefixMapping (String prefix) throws SAXException; //解析到標籤事件 public void startElement (String uri, String localName, String qName, Attributes atts) throws SAXException; //標籤結束事件 public void endElement (String uri, String localName, String qName) throws SAXException; //標籤中內容事件 public void characters (char ch[], int start, int length) throws SAXException; //可忽略的空格事件 public void ignorableWhitespace (char ch[], int start, int length) throws SAXException; //處理指令事件 public void processingInstruction (String target, String data) throws SAXException; //忽略標籤事件 public void skippedEntity (String name) throws SAXException; }
對應HtmlToSpannedConverter中的實現。
public void setDocumentLocator(Locator locator) {}public void startDocument() throws SAXException {}public void endDocument() throws SAXException {}public void startPrefixMapping(String prefix, String uri) throws SAXException {}public void endPrefixMapping(String prefix) throws SAXException {}public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { handleStartTag(localName, attributes); }public void endElement(String uri, String localName, String qName) throws SAXException { handleEndTag(localName); }public void characters(char ch[], int start, int length) throws SAXException { //忽略 ... }public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {}public void processingInstruction(String target, String data) throws SAXException {}public void skippedEntity(String name) throws SAXException {}
我們發現該類中只實現了startElement,endElement,characters這三個方法,所以只關心標籤的型別和標籤裡的字元。然後呼叫mReader.parse方法,開始對HTML進行解析。解析的事件流如下: startElement -> characters -> endElement startElemnt裡面呼叫的是handleStartTag方法,endElement則是呼叫handleEndTag方法。
/** * @param tag:標籤型別 * @param attributes:屬性值 * 例如遇到標籤,tag="font",attributes={"color":"#FFFFFF"} */private void handleStartTag(String tag, Attributes attributes) { if (tag.equalsIgnoreCase("br")) { // 我們不需要關心br標籤是否有閉合,因為Tagsoup會幫我們處理 } else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("div")) { handleP(mSpannableStringBuilder); } else if (tag.equalsIgnoreCase("strong")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("b")) { start(mSpannableStringBuilder, new Bold()); } else if (tag.equalsIgnoreCase("em")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("cite")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("dfn")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("i")) { start(mSpannableStringBuilder, new Italic()); } else if (tag.equalsIgnoreCase("big")) { start(mSpannableStringBuilder, new Big()); } else if (tag.equalsIgnoreCase("small")) { start(mSpannableStringBuilder, new Small()); } else if (tag.equalsIgnoreCase("font")) { startFont(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("blockquote")) { handleP(mSpannableStringBuilder); start(mSpannableStringBuilder, new Blockquote()); } else if (tag.equalsIgnoreCase("tt")) { start(mSpannableStringBuilder, new Monospace()); } else if (tag.equalsIgnoreCase("a")) { startA(mSpannableStringBuilder, attributes); } else if (tag.equalsIgnoreCase("u")) { start(mSpannableStringBuilder, new Underline()); } else if (tag.equalsIgnoreCase("sup")) { start(mSpannableStringBuilder, new Super()); } else if (tag.equalsIgnoreCase("sub")) { start(mSpannableStringBuilder, new Sub()); } else if (tag.length() == 2 && Character.toLowerCase(tag.charAt(0)) == 'h' && tag.charAt(1) >= '1' && tag.charAt(1) = '1' && tag.charAt(1)從上面方法中我們可以總結出支援的HTML標籤列表
br
p
div
strong
b
em
cite
dfn
i
big
small
font
blockquote
tt
monospace
a
u
sup
sub
h1-h6
img
標籤是如何處理的
br標籤
這裡分析如何處理
標籤,在handleStartTag方法中可以發現br標籤直接被忽略了,在handleEndTag方法中才被真正處理。private void handleEndTag(String tag) { ... if (tag.equalsIgnoreCase("br")) { handleBr(mSpannableStringBuilder); } ... }//程式碼很簡單,直接加換行符private static void handleBr(SpannableStringBuilder text) { text.append("n"); }p標籤
p標籤為段落,其作用是給p標籤中的文字前後換行,在handleStartTag和handleEndTag遇到p標籤都是呼叫handleP方法,characters則新增p標籤之間的字串。
private void handleStartTag(String tag, Attributes attributes) { ... else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } ... }private void handleEndTag(String tag) { ... else if (tag.equalsIgnoreCase("p")) { handleP(mSpannableStringBuilder); } ... }private static void handleP(SpannableStringBuilder text) { int len = text.length(); if (len >= 1 && text.charAt(len - 1) == 'n') { if (len >= 2 && text.charAt(len - 2) == 'n') { //如果前面兩個字元都為換行符,則忽略 return; } //否則新增一個換行符 text.append("n"); return; } //其他情況新增兩個換行符 if (len != 0) { text.append("nn"); } }strong標籤
該標籤作用是為加粗字型,在handleStartTag和handleEndTag分別呼叫start和end方法。
private void handleStartTag(String tag, Attributes attributes) { ... else if (tag.equalsIgnoreCase("strong")) { start(mSpannableStringBuilder, new Bold()); } ... }private static class Bold { }//什麼都沒有private void handleEndTag(String tag) { ... else if (tag.equalsIgnoreCase("strong")) { end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD)); } ... }private static void start(SpannableStringBuilder text, Object mark) { int len = text.length(); //mark作為型別標記並沒有實際功能,指明開始的位置, //結束位置延遲到`end`方法中處理, //Spannable.SPAN_MARK_MARK表示當文字插入偏移時,它們仍然保持在它們的原始偏移量上。從概念上講,文字是在標記之後新增的。 text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); } private static void end(SpannableStringBuilder text, Class kind,Object repl) { //當前字元長度 int len = text.length(); //根據kind獲取最後一個set進去的物件 Object obj = getLast(text, kind); //獲取標籤起始位置 int where = text.getSpanStart(obj); //去除標記物件 text.removeSpan(obj); if (where != len) { //len則為結束的位置,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE是設定樣式文字區間為閉區間 //將真正的樣式物件repl設定進去,Bold對應StyleSpan型別,Typeface.BOLD 加粗樣式 text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } }private static Object getLast(Spanned text, Class kind) { /* * 獲取最後一個型別為king,在setSpan傳入的物件 * 例如kind型別為Bold.class,則會返回在start中set進去的Bold物件 */ Object[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } else { //如果有期間有多個,則獲取最後一個 return objs[objs.length - 1]; } }經過start和end方法處理後,strong標籤中的文字就被加粗,具體的樣式型別這裡不做詳解,後續可以參考Spannable原始碼解析這篇目前還沒人認領文章,其他為字型設定不同的樣式過程一致,在handleStartTag根據不同標籤型別呼叫start時方法傳入不同物件給mark,並在handleEndTag中不同標籤呼叫end並傳入不同樣式。
font標籤
font標籤可以給字串指定顏色和字型。
private void handleStartTag(String tag, Attributes attributes) { ... else if (tag.equalsIgnoreCase("font")) { //attributes帶有標籤中的屬性 //例如,屬性將以key-value的形式存在,{"color":"#FFFFFF"}。 startFont(mSpannableStringBuilder, attributes); } ... }private static void startFont(SpannableStringBuilder text,Attributes attributes) { String color = attributes.getValue("", "color");//獲取color屬性 String face = attributes.getValue("", "face");//獲取face屬性 int len = text.length(); //Font同樣是一個用來標記屬性的物件,沒有實際功能 text.setSpan(new Font(color, face), len, len, Spannable.SPAN_MARK_MARK); }//儲存顏色值和字型型別private static class Font { public String mColor; public String mFace; public Font(String color, String face) { mColor = color; mFace = face; } }private void handleEndTag(String tag) { ... else if (tag.equalsIgnoreCase("font")) { endFont(mSpannableStringBuilder); } ... }private static void endFont(SpannableStringBuilder text) { int len = text.length(); Object obj = getLast(text, Font.class); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { Font f = (Font) obj; //前面與strong標籤解析過程相似,多了下面處理顏色和字型的邏輯 if (!TextUtils.isEmpty(f.mColor)) { //如果color屬性中以"@"開頭,則是獲取colorId對應的顏色值 //注意:只能支援android.R的資源 if (f.mColor.startsWith("@")) { Resources res = Resources.getSystem(); String name = f.mColor.substring(1); int colorRes = res.getIdentifier(name, "color", "android"); if (colorRes != 0) { //也可以是color selector,則會根據不同狀態顯示不同顏色 ColorStateList colors = res.getColorStateList(colorRes, null); //1、透過TextAppearanceSpan設定顏色 text.setSpan(new TextAppearanceSpan(null, 0, 0, colors, null), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } else { //如果為"#"開頭則解析顏色值 int c = Color.getHtmlColor(f.mColor); if (c != -1) { //2、透過ForegroundColorSpan直接設定字型的rgb值 text.setSpan(new ForegroundColorSpan(c | 0xFF000000), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } if (f.mFace != null) { //如果有face引數則透過TypefaceSpan設定字型 text.setSpan(new TypefaceSpan(f.mFace), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } }具體支援哪些字型,在TypefaceSpan的apply方法中會先去解析對應的字型,然後繪製出來,原始碼如下。
private static void apply(Paint paint, String family) { ... //解析字型 Typeface tf = Typeface.create(family, oldStyle); ... }Typeface原始碼
/** * 根據字型名稱獲取字型物件,如果familyName為null,則返回預設字型物件 * 呼叫getStyle可檢視該字型style屬性 * * @param 字型名稱,可能為null * @param style NORMAL(標準), BOLD(粗體), ITALIC(斜體), BOLD_ITALIC(粗斜) * @return 匹配的字型 */public static Typeface create(String familyName, int style) { if (sSystemFontMap != null) { //字型快取在sSystemFontMap中 return create(sSystemFontMap.get(familyName), style); } return null; } //init方法中初始化sSystemFontMap private static void init() { // 獲取字型配置檔案目錄 //private static File getSystemFontConfigLocation() { //return new File("/system/etc/"); //} File systemFontConfigLocation = getSystemFontConfigLocation(); //獲取字型配置檔案 //static final String FONTS_CONFIG = "fonts.xml"; File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); try { //將字型名稱更Typeface物件快取在map中 //具體解析過程忽略,有興趣可自行翻閱原始碼 .... sSystemFontMap = systemFonts; } catch (RuntimeException e) { .... } }img標籤
//img標籤只有在標籤開始時處理
private void handleStartTag(String tag, Attributes attributes) {...else if (tag.equalsIgnoreCase("img")) { startImg(mSpannableStringBuilder, attributes, mImageGetter); } ...}
//與其他標籤處理過程多了Attributes標籤屬性,Html.ImageGetter 自定義圖片獲取
private static void startImg(SpannableStringBuilder text, Attributes attributes, Html.ImageGetter img) { //獲取src屬性 String src = attributes.getValue("", "src"); Drawable d = null; if (img != null) { //呼叫自定義的圖片獲取方式,並傳入src屬性值 d = img.getDrawable(src); } if (d == null) { //如果圖片為空,則返回一個小方塊 d = Resources.getSystem(). getDrawable(com.android.internal.R.drawable.unknown_image); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); } int len = text.length(); //新增圖片佔位字元 text.append("uFFFC"); //透過使用ImageSpan設定圖片效果 text.setSpan(new ImageSpan(d, src), len, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); }自定義標籤
private void handleStartTag(String tag, Attributes attributes) { ... else if (mTagHandler != null) {//透過自定義標籤處理器來擴充套件自定義標籤 mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); } ... }private void handleEndTag(String tag) { ... else if (mTagHandler != null) { //閉合標籤 mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader); } ... }關於自定義標籤有個小問題是,handleTag並沒有傳入Attributes標籤屬性,所以無法直接獲取自定義標籤的屬性值,下面給出兩種方案解決這個問題:
1.透過某一部分標籤名作為屬性值,例如
標籤,我們想加入id的引數,則可將標籤名變為 ,然後在handleTag中自行解析。
2.透過反射XMLReader來獲取屬性值,具體例子可參考stackoverflow:How to get an attribute from an XMLReaderconvert方法剩下部分
不要忽略了parse之後還有一部分程式碼。
// 修正段落標記範圍
//ParagraphStyle為段落級別樣式Object[] obj = mSpannableStringBuilder.getSpans(0, mSpannableStringBuilder.length(), ParagraphStyle.class);for (int i = 0; i = 0) { if (mSpannableStringBuilder.charAt(end - 1) == 'n' && mSpannableStringBuilder.charAt(end - 2) == 'n') { end--; } } if (end == start) { //除去沒有顯示的樣式 mSpannableStringBuilder.removeSpan(obj[i]); } else { //Spannable.SPAN_PARAGRAPH以換行符為起始點和終點 mSpannableStringBuilder.setSpan(obj[i], start, end, Spannable.SPAN_PARAGRAPH); } }return mSpannableStringBuilder;toHtml解析
樣式級別主要分為兩類:一類是段落級別,另一類是字元級別。 toHtml方法是從大範圍級別到小範圍級別解析,即先解析段落樣式再解析字元樣式。
public static String toHtml(Spanned text) { StringBuilder out = new StringBuilder(); withinHtml(out, text); return out.toString(); }private static void withinHtml(StringBuilder out, Spanned text) {int len = text.length();//遍歷段落級別樣式int next;for (int i = 0; i 標籤,align屬性代表段落中字元對齊方向 for(int j = 0; j "); } //解析段落樣式中其他樣式 withinDiv(out, text, i, next); //呼叫層次較深,先忽略後面 .... }//start為段落起點,end為結尾private static void withinDiv(StringBuilder out, Spanned text,int start, int end) { int next; for (int i = start; i "); } //解析裡面的樣式 withinBlockquote(out, text, i, next); //忽略 .... } }private static void withinBlockquote(StringBuilder out, Spanned text,int start, int end) {//解析文字顯示方向out.append(getOpenParaTagWithDirection(text, start, end)); ...int next;for (int i = start; i"); } if (style[j] instanceof AbsoluteSizeSpan) { out.append(""); } if (style[j] instanceof URLSpan) { out.append(""); } if (style[j] instanceof StrikethroughSpan) { out.append(""); } if (style[j] instanceof UnderlineSpan) { out.append(""); } if (style[j] instanceof SubscriptSpan) { out.append(""); } if (style[j] instanceof SuperscriptSpan) { out.append(""); } if (style[j] instanceof TypefaceSpan) { String s = ((TypefaceSpan) style[j]).getFamily(); if (s.equals("monospace")) { out.append(""); } } if (style[j] instanceof StyleSpan) { int s = ((StyleSpan) style[j]).getStyle(); if ((s & Typeface.BOLD) != 0) { out.append(""); } if ((s & Typeface.ITALIC) != 0) { out.append(""); } } } } //新增br標籤 if (nl == 1) { out.append("private static String getOpenParaTagWithDirection(Spanned text, int start, int end) { final int len = end - start; final byte[] levels = ArrayUtils.newUnpaddedByteArray(len);//透過VMRuntime建立byte陣列 final char[] buffer = TextUtils.obtain(len);//底層透過ArrayUtils.newUnpaddedCharArray(len);建立char陣列 TextUtils.getChars(text, start, end, buffer, 0);//將字元快取到buffer陣列中 //透過bidi來解析文字方向,作用是相容多國語言的排列方向,例如阿利伯文字排列是自右向左。 //裡面主要呼叫native方法,具體可參考系統原始碼。 int paraDir = AndroidBidi.bidi(Layout.DIR_REQUEST_DEFAULT_LTR, buffer, levels, len,false /* no info */); //使用p標籤加dir屬性修飾文字方向 switch(paraDir) { case Layout.DIR_RIGHT_TO_LEFT: return ""; case Layout.DIR_LEFT_TO_RIGHT: default: return "
"; } }
private static void withinBlockquote(StringBuilder out, Spanned text,int start, int end) { //解析文字顯示方向 out.append(getOpenParaTagWithDirection(text, start, end)); //回來 int next; for (int i = start; i//程式碼較多,擷取前部分//start為起始位置,end為除去換行符文字結尾位置,nl換行符個數,last是否為文字末尾private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) { int next; for (int i = start; i "); } //斜體使用i標籤包裹 if ((s & Typeface.ITALIC) != 0) { out.append(""); } } //TypefaceSpan樣式 if (style[j] instanceof TypefaceSpan) { //獲取字型型別 String s = ((TypefaceSpan) style[j]).getFamily(); //monospace使用tt標籤包裹 if ("monospace".equals(s)) { out.append(""); } } //上標樣式使用sup標籤包裹 if (style[j] instanceof SuperscriptSpan) { out.append(""); } //小標使用sub標籤包裹 if (style[j] instanceof SubscriptSpan) { out.append(""); } //下劃線使用u標籤變過 if (style[j] instanceof UnderlineSpan) { out.append(""); } //刪除線使用strike標籤 if (style[j] instanceof StrikethroughSpan) { out.append(""); } //urlspan使用a標籤加href屬性的標籤包裹 if (style[j] instanceof URLSpan) { out.append(""); } //圖片使用img加src屬性的標籤包裹 if (style[j] instanceof ImageSpan) { out.append(""); // Don't output the dummy character underlying the image. i = next; } //絕對大小樣式使用font加size屬性標籤包裹 if (style[j] instanceof AbsoluteSizeSpan) { out.append(""); } //前景色使用font加color屬性標籤,只支援# //不支援@描述的資源顏色,因為@使用TextAppearanceSpan型別 if (style[j] instanceof ForegroundColorSpan) { out.append(""); } } withinStyle(out, text, i, next); .... } }//將字元進行轉碼private static void withinStyle(StringBuilder out, CharSequence text, int start, int end) { for (int i = start; i & '空格' 變成轉譯字元 if (c == '') { out.append(">"); } else if (c == '&') { out.append("&"); } else if (c >= 0xD800 && c = 0xDC00 && d 0x7E || c//跳出withinStyle接著回到withinParagraph方法private static boolean withinParagraph(StringBuilder out, Spanned text, int start, int end, int nl, boolean last) { ...//接下來很好理解,把上面包裹的標籤閉合for (int j = style.length - 1; j >= 0; j--) { if (style[j] instanceof ForegroundColorSpan) { out.append("
n"); return false; } else { for (int i = 2; i "); } return !last; } }
//跳出withinParagraph回到withinBlockquote剩下部分private static void withinBlockquote(StringBuilder out, Spanned text,int start, int end) { ... if (withinParagraph(out, text, i, next - nl, nl, next == end)) { /* 閉合p標籤 */ out.append("n"); out.append(getOpenParaTagWithDirection(text, next, end));//判斷字元排列順序,新增p標籤 } //閉合p標籤 out.append("n"); } private static void withinDiv(StringBuilder out, Spanned text, int start, int end) { ... withinBlockquote(out, text, i, next); //逐個閉合 for (QuoteSpan quote : quotes) { out.append("n"); } } }
private static void withinHtml(StringBuilder out, Spanned text) { ... withinDiv(out, text, i, next); //按需要新增閉合div標籤,即有AlignmentSpan型別樣式 if (needDiv) { out.append("
escapeHtml解析
public static String escapeHtml(CharSequence text) { StringBuilder out = new StringBuilder(); //直接呼叫withinStyle方法進行轉譯 withinStyle(out, text, 0, text.length()); return out.toString(); }
原文連結:http://www.apkbus.com/blog-902332-68287.html
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3705/viewspace-2814182/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android初學路上會遇到的瓶頸【安卓巴士博文大賽】Android安卓
- WebView 顯示HTML富文字WebViewHTML
- 影片直播原始碼,Flutter 富文字 部分文字可以觸發點選原始碼Flutter
- Android Handler 原始碼解析Android原始碼
- Android Retrofit原始碼解析Android原始碼
- Android——LruCache原始碼解析Android原始碼
- android LruCache原始碼解析Android原始碼
- Android EventBus原始碼解析Android原始碼
- android原始碼解析--switchAndroid原始碼
- Android原始碼解析--LooperAndroid原始碼OOP
- Android 開源專案原始碼解析 -->PhotoView 原始碼解析(七)Android原始碼View
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Android原始碼解析-LiveDataAndroid原始碼LiveData
- [Android] Retrofit原始碼:流程解析Android原始碼
- Android 8.1 Handler 原始碼解析Android原始碼
- Android LayoutInflater 原始碼解析Android原始碼
- WebRTC-Android原始碼解析WebAndroid原始碼
- android原始碼解析--DialogAndroid原始碼
- android原始碼解析--MessageQueueAndroid原始碼
- android原始碼解析--MessageAndroid原始碼
- Android fragment原始碼全解析AndroidFragment原始碼
- android原始碼解析--ListView(上)Android原始碼View
- Android 開源專案原始碼解析 -->Volley 原始碼解析(十五)Android原始碼
- Android 開源專案原始碼解析 -->Dagger 原始碼解析(十三)Android原始碼
- HTML 頁面使用 wangeditor 富文字編輯器HTML
- Android 開源專案原始碼解析 -->CircularFloatingActionMenu 原始碼解析(八)Android原始碼
- 數千個Android專案原始碼安卓遊戲原始碼大全經典安卓專案附帶原始碼(圖片版)Android原始碼安卓遊戲
- Android LayoutInflater Factory 原始碼解析Android原始碼
- Android setContentView原始碼解析AndroidView原始碼
- Android Volley框架原始碼解析Android框架原始碼
- 各種富文字/ HTML編輯器和框架比較HTML框架
- Squire – 簡潔的 HTML5 富文字編輯器UIHTML
- 輕量級富文字編輯器wangEditor原始碼結構介紹原始碼
- Android系統原始碼目錄解析Android原始碼
- Android 網路框架 Retrofit 原始碼解析Android框架原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- weex原始碼解析(四)- android引入sdk原始碼Android
- Android原始碼解析(二)動畫篇-- ObjectAnimatorAndroid原始碼動畫Object