Android中的TextView,本身就支持部分的Html格式標簽。這其中包括常用的字體大小顏色設置,文本鏈接等。使用起來也比較方便,只需要使用Html類轉換一下即可。比如:
textView.setText(Html.fromHtml(str));
然而,有一種場合,默認支持的標簽可能不夠用。比如,我們需要在textView中點擊某種鏈接,返回到應用中的某個界面,而不僅僅是網絡連接,如何實現?
經過幾個小時對android中的Html類源代碼的研究,找到了解決辦法,並且測試通過。
先看Html類的源代碼中有這樣一段:
- /**
- * Is notified when HTML tags are encountered that the parser does
- * not know how to interpret.
- */
- public static interface TagHandler {
- /**
- * This method will be called whenn the HTML parser encounters
- * a tag that it does not know how to interpret.
- */
- public void handleTag(boolean opening, String tag,
- Editable output, XMLReader xmlReader);
這里定義了一個接口,接口用於什么呢?
再繼續看代碼,看到對Html的tag進行解析部分的代碼:
- private void handleStartTag(String tag, Attributes attributes) {
- if (tag.equalsIgnoreCase("br")) {
- // We don't need to handle this. TagSoup will ensure that there's a </br> for each <br>
- // so we can safely emite the linebreaks when we handle the close tag.
- } else if (tag.equalsIgnoreCase("p")) {
- handleP(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("div")) {
- handleP(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("em")) {
- start(mSpannableStringBuilder, new Bold());
- } else if (tag.equalsIgnoreCase("b")) {
- start(mSpannableStringBuilder, new Bold());
- } else if (tag.equalsIgnoreCase("strong")) {
- 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) <= '6') {
- handleP(mSpannableStringBuilder);
- start(mSpannableStringBuilder, new Header(tag.charAt(1) - '1'));
- } else if (tag.equalsIgnoreCase("img")) {
- startImg(mSpannableStringBuilder, attributes, mImageGetter);
- } else if (mTagHandler != null) {
- mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);
- }
- }
- private void handleEndTag(String tag) {
- if (tag.equalsIgnoreCase("br")) {
- handleBr(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("p")) {
- handleP(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("div")) {
- handleP(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("em")) {
- end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
- } else if (tag.equalsIgnoreCase("b")) {
- end(mSpannableStringBuilder, Bold.class, new StyleSpan(Typeface.BOLD));
- } else if (tag.equalsIgnoreCase("strong")) {
- end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
- } else if (tag.equalsIgnoreCase("cite")) {
- end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
- } else if (tag.equalsIgnoreCase("dfn")) {
- end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
- } else if (tag.equalsIgnoreCase("i")) {
- end(mSpannableStringBuilder, Italic.class, new StyleSpan(Typeface.ITALIC));
- } else if (tag.equalsIgnoreCase("big")) {
- end(mSpannableStringBuilder, Big.class, new RelativeSizeSpan(1.25f));
- } else if (tag.equalsIgnoreCase("small")) {
- end(mSpannableStringBuilder, Small.class, new RelativeSizeSpan(0.8f));
- } else if (tag.equalsIgnoreCase("font")) {
- endFont(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("blockquote")) {
- handleP(mSpannableStringBuilder);
- end(mSpannableStringBuilder, Blockquote.class, new QuoteSpan());
- } else if (tag.equalsIgnoreCase("tt")) {
- end(mSpannableStringBuilder, Monospace.class,
- new TypefaceSpan("monospace"));
- } else if (tag.equalsIgnoreCase("a")) {
- endA(mSpannableStringBuilder);
- } else if (tag.equalsIgnoreCase("u")) {
- end(mSpannableStringBuilder, Underline.class, new UnderlineSpan());
- } else if (tag.equalsIgnoreCase("sup")) {
- end(mSpannableStringBuilder, Super.class, new SuperscriptSpan());
- } else if (tag.equalsIgnoreCase("sub")) {
- end(mSpannableStringBuilder, Sub.class, new SubscriptSpan());
- } else if (tag.length() == 2 &&
- Character.toLowerCase(tag.charAt(0)) == 'h' &&
- tag.charAt(1) >= '1' && tag.charAt(1) <= '6') {
- handleP(mSpannableStringBuilder);
- endHeader(mSpannableStringBuilder);
- } else if (mTagHandler != null) {
- mTagHandler.handleTag(false, tag, mSpannableStringBuilder, mReader);
- }
- }
可以看到,如果不是默認的標簽,會調用mTagHandler的handleTag方法。所以,我們可以實現此接口,來解析自己定義的標簽類型。
再看一段我實現的對<game>標簽進行解析的示例代碼:
- public class GameTagHandler implements TagHandler {
- private int startIndex = 0;
- private int stopIndex = 0;
- @Override
- public void handleTag(boolean opening, String tag, Editable output,
- XMLReader xmlReader) {
- if (tag.toLowerCase().equals("game")) {
- if (opening) {
- startGame(tag, output, xmlReader);
- } else {
- endGame(tag, output, xmlReader);
- }
- }
- }
- public void startGame(String tag, Editable output, XMLReader xmlReader) {
- startIndex = output.length();
- }
- public void endGame(String tag, Editable output, XMLReader xmlReader) {
- stopIndex = output.length();
- output.setSpan(new GameSpan(), startIndex, stopIndex,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- private class GameSpan extends ClickableSpan implements OnClickListener {
- @Override
- public void onClick(View v) {
- // 跳轉某頁面
- }
- }
上面這段代碼,是對<game>…</game>的自定義標簽進行解析。
具體調用方法:
textView.setText(Html.fromHtml(“點擊<game>這里</game>跳轉到游戲”,
null, new GameTagHandler()));
textView.setClickable(true);
textView.setMovementMethod(LinkMovementMethod.getInstance());
運行后,能夠看到文本中的字符串“這里”帶了超鏈接,點擊鏈接后,GameSpan類的onClick()方法被調用。就可以在這個方法中進行跳轉了。
看了一下第一種方式,直接使用SpannableString明顯是不可行的,因為我們必須知道他的具體長度,那么只能夠換一種方式實現了,相信有寫過Html的大神們都知道其實Android有一個類叫Html,里面是支持我們Html格式的字符串轉換為文本的,那么這時候思路就很清晰了,我們只需要接收Html格式的String,然后使用Html.fromHtml方法就可以將他轉換為我們想要的多樣式TextView!,馬上動手試試。
3.3、代碼
我們定義一個String假裝他是服務器傳遞過來的數據進行顯示看看結果是怎么樣的,首先是來一個html格式的字符串
String htmlStr = "<font color='#0000FF' size='50px'>我是藍色的文本</font><br><font color='#ff0000' size='40px'>我是紅色的文本</font><br><font color='#000000' size='29px'>我是黑色的文本</font>"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = new TextView(this); mTextView.setText(Html.fromHtml(htmlStr)); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
哈哈,這么簡單的代碼對於我們來說不就是分分鍾就搞定嗎,馬上就來驗證一下自己的成果
我擦,顏色出來了,但是字體大小怎么完全沒改變,你是不是在逗我,憑我多年寫Html的Hello World語句來說我的Html文本肯定沒有錯!,立馬找一下原因是為什么,讓我們來看看Html.fromHtml都做了什么事情。

從源碼里可以看到他會去定義一個SAX解析類Parser,然后傳遞到133行HtmlToSpannedConverter的構造方法里,並且調用這個類的conver()方法,那我們先看看這個類里面都做了什么 
簡單過一下構造方法,知道他都有什么

如果有寫過SAX解析的朋友現在肯定不會陌生,首先是去設置一個文檔內容的處理器,進行XML的解析,里面就是一些頭節點尾節點元素開頭結束等等的XML相關處理,然后調用parser進行解析,然后會走回調方法,就列出我們比較關心的頭尾方法

然后進行節點的處理


這時候眼睛比較凌厲的朋友已經發現我們最想要知道的代碼了!他是如何去處理font這個標簽的,讓我們來看看這個方法startFont(mSpannableStringBuilder, attributes);

看到這里的時候我的心里是奔潰的。。尼瑪這都什么跟什么,怎么就支持color這個標簽,不支持size,還有這face是什么鬼,能支持face難道不能支持我傳說中的大size么!!簡直是在逗我!!
在這里我們可以看出來他的實現方式其實很簡單,首先是使用XML去解析每一個節點,然后使用SpannableStringBuilder去進行拼接。
到了這個時候我們只能夠自己去定義實現font以及獲取里面的屬性了,要怎么做呢,我們可以看到其實他在這一大堆if else的判斷里面已經把font這個標簽給處理掉了,不會給我們繼續處理(不要跟我說修改源碼),這時候其實我們看一下if else的最后,他是會進行回調到一個叫TagHandler里面的方法的,那么我們只需要去實現這個接口就可以了,從上面可以看出來,他是一個抽象類,用於給我們擴展的。
上面說到了font標簽已經被處理掉了,不會再回調給我們,所以我們就需要自己去定義一個標簽,當然了,后端給予我們得一樣可以是font的,我們只是自己去進行替換,類似這樣子
htmlStr = htmlStr.replaceAll("font", "bluefont"); mTextView.setText(Html.fromHtml(htmlStr, null, new HtmlTagHandler()));
- 1
- 2
首先是替換成一個源碼里不會進行處理的標簽
/** * Created by blue. */ public class HtmlTagHandler implements Html.TagHandler { private static final String TAG_BLUE_FONT = "bluefont"; private int startIndex = 0; private int stopIndex = 0; final HashMap<String, String> attributes = new HashMap<String, String>(); @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { processAttributes(xmlReader); if(tag.equalsIgnoreCase(TAG_BLUE_FONT)){ if(opening){ startFont(tag, output, xmlReader); }else{ endFont(tag, output, xmlReader); } } } public void startFont(String tag, Editable output, XMLReader xmlReader) { startIndex = output.length(); } public void endFont(String tag, Editable output, XMLReader xmlReader){ stopIndex = output.length(); String color = attributes.get("color"); String size = attributes.get("size"); size = size.split("px")[0]; if(!TextUtils.isEmpty(color) && !TextUtils.isEmpty(size)){ output.setSpan(new ForegroundColorSpan(Color.parseColor(color)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } if(!TextUtils.isEmpty(size)){ output.setSpan(new AbsoluteSizeSpan(Integer.parseInt(size)), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } private void processAttributes(final XMLReader xmlReader) { try { Field elementField = xmlReader.getClass().getDeclaredField("theNewElement"); elementField.setAccessible(true); Object element = elementField.get(xmlReader); Field attsField = element.getClass().getDeclaredField("theAtts"); attsField.setAccessible(true); Object atts = attsField.get(element); Field dataField = atts.getClass().getDeclaredField("data"); dataField.setAccessible(true); String[] data = (String[])dataField.get(atts); Field lengthField = atts.getClass().getDeclaredField("length"); lengthField.setAccessible(true); int len = (Integer)lengthField.get(atts); for(int i = 0; i < len; i++){ attributes.put(data[i * 5 + 1], data[i * 5 + 4]); } } catch (Exception e) { } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
使用startIndex和stopIndex來進行判斷每一個標簽頭和尾的位置,對里面的文本進行相對應的樣式處理。然后讓我們來運行一下代碼試試
恩!?為什么第一行的樣式不起效果呢??其實這個只是SAX解析的一些小bug而已,具體原理的話稍后我再貼上來,解決的方案也很簡單:
1)在這一段Html格式的字符串前面再加上隨意一個標簽,例如<p>標簽等等
2)發送html格式的字符串過來的時候將<html><body>也就是一整個網頁需要的信息傳遞過來,也可以解決這個問題
那么修復了這個小bug后讓我們來看看我們的最終成功,一個字段顯示多種不同樣式的文本 
四、總結
上面的這些都是拋磚引玉,帶來一些思路,我們可以自己進行擴展所有的Html標簽,Android自帶能支持的標簽實在是太少了,而且連</br>都不能帶斜杠得寫成<br>不然不能正常得換行。
Spannable setSpan用到的這個類,百度多了解下
