Android富文字Html原始碼解析【安卓巴士博文大賽】

starkbl發表於2021-09-09

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/>" +             "大字型
"+             "小字型
"+             "加粗
"+             "斜體
" +             "

標題一

" +             "

標題二

" +             "Android富文字Html原始碼解析【安卓巴士博文大賽】" +             "
引用
" +             "
" +             "下劃線
" +             "上標正常字型下標
" +             "樣式";     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 {

/**
 * 為Android富文字Html原始碼解析【安卓巴士博文大賽】標籤提供圖片檢索功能
 */public static interface ImageGetter {    /**
     * 當HTML解析器解析到Android富文字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() { }/**
 * 返回樣式文字,所有Android富文字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 XMLReader

convert方法剩下部分

不要忽略了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 
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("Android富文字Html原始碼解析【安卓巴士博文大賽】");                // 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("
");             }            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("
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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章