一、怎么用?
先介紹TextView富文本的使用方法,TextView富文本顯示主要有兩種方式,一個是使用SpannableString類,另一種是直接將富文本寫成HTML形式。
SpannableString
SpannableString是Android內(nèi)置的專門處理富文本的類,基本涵蓋了你能想到的所有富文本表示,字體、顏色、圖片、點擊事件…功能非常強(qiáng)大。話不多說,直接上代碼:
示例
//設(shè)置Hello World前三個字符為紅色,背景為藍(lán)色
SpannableString textSpanned1 = new SpannableString("Hello World");
textSpanned1.setSpan(new ForegroundColorSpan(Color.RED),
0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textSpanned1.setSpan(new BackgroundColorSpan(Color.BLUE),
0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text1.setText(textSpanned1);
//設(shè)置Hello World前三個字符字體為斜體
SpannableString textSpanned2 = new SpannableString("Hello World");
textSpanned2.setSpan(new StyleSpan(Typeface.ITALIC),
0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text2.setText(textSpanned2);
//設(shè)置Hello World前三個字符有下劃線
SpannableString textSpanned3 = new SpannableString("Hello World");
textSpanned3.setSpan(new UnderlineSpan(),
0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text3.setText(textSpanned3);
//設(shè)置Hello World前三個字符有點擊事件
SpannableStringBuilder textSpanned4 = new SpannableStringBuilder("Hello World");
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "Hello World", Toast.LENGTH_SHORT).show();
}
};
textSpanned4.setSpan(clickableSpan,
0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//注意:此時必須加這一句,不然點擊事件不會生效
text4.setMovementMethod(LinkMovementMethod.getInstance());
text4.setText(textSpanned4);

setSpan()
void setSpan (Object what, int start, int end, int flags)
| 參數(shù) | 說明 |
|---|---|
| what | 樣式 |
| start | 樣式開始的字符索引 |
| end | 樣式結(jié)束的字符索引 |
| flags | 新插入字符的設(shè)置 |
flags:
| 取值 | 說明 |
|---|---|
| Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | 前后都不包括 |
| Spanned.SPAN_EXCLUSIVE_INCLUSIVE | 前面不包括,后面包括 |
| Spanned.SPAN_INCLUSIVE_EXCLUSIVE | 前面包括,后面不包括 |
| Spanned.SPAN_INCLUSIVE_INCLUSIVE | 前后都包括 |
這個flags可能有人不懂,它表示了這個樣式是否作用在本字符串之前或之后插入的其他字符串上,舉個例子:
SpannableStringBuilder textSpannedBuilder1 = new SpannableStringBuilder();
SpannableString textSpanned11 = new SpannableString("Hello");
textSpanned11.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned11.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString textSpanned12 = new SpannableString("World");
text1.setText(textSpannedBuilder1.append(textSpanned11).append(textSpanned12));
SpannableStringBuilder textSpannedBuilder2 = new SpannableStringBuilder();
SpannableString textSpanned21 = new SpannableString("Hello");
textSpanned21.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned21.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
SpannableString textSpanned22 = new SpannableString("World");
text2.setText(textSpannedBuilder2.append(textSpanned21).append(textSpanned22));
SpannableStringBuilder textSpannedBuilder3 = new SpannableStringBuilder();
SpannableString textSpanned31 = new SpannableString("Hello");
textSpanned31.setSpan(new BackgroundColorSpan(Color.BLUE), 0, textSpanned21.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
SpannableString textSpanned32 = new SpannableString("World");
textSpanned32.setSpan(new BackgroundColorSpan(Color.GREEN), 0, 3, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
text3.setText(textSpannedBuilder3.append(textSpanned31).append(textSpanned32));

text1里,"Hello"的flags是SPAN_EXCLUSIVE_EXCLUSIVE,在它之后插入的"World"顯示正常,無背景。
text2里,"Hello"的flags是SPAN_EXCLUSIVE_INCLUSIVE,它之后插入的"World"的背景變?yōu)樗{(lán)色。
需要注意的是text3,這里的"Hello"與text2相同,而"World"的一部分字符設(shè)置為綠色,顯然這部分字符顯示的是綠色,這說明雖然設(shè)置了SPAN_EXCLUSIVE_INCLUSIVE屬性,但只要后面的字符串設(shè)置了同類的樣式,還是覆蓋掉flags屬性。
SpannableString和SpannableStringBuilder
在上面的例子中我們用到了SpannableStringBuilder類,那這個類和SpannableString有什么不同呢?其實大家只要想想String和StringBuilder區(qū)別就行了,SpannableString在創(chuàng)建的時候就需要指定好字符串,之后就不能更改了,而SpannableStringBuilder可以使用append()方法,在已有的富文本后添加新的富文本。
HTML
接下來介紹HTML的用法,其實HTML使用起來要比SpannableString簡潔,我們只需要按照平時寫HTML的習(xí)慣,將需要顯示的富文本加上各種標(biāo)簽,就可以顯示在TextView上了,下面我們看一下例子:
示例
String htmlText1 = "<b>Hello World</b>";
text1.setText(Html.fromHtml(htmlText1));
String htmlText2 = "<font color='#ff0000'>Hello World</font>";
text2.setText(Html.fromHtml(htmlText2));
String htmlText3 = "<i><a ;
text3.setMovementMethod(LinkMovementMethod.getInstance());
text3.setText(Html.fromHtml(htmlText3));

是不是感覺比SpannableString簡單多了,其實Html類內(nèi)部還是在使用Spannable處理,我們會在后文看到它的實現(xiàn)方式。
TextView支持的HTML標(biāo)簽
| 標(biāo)簽 | 說明 |
|---|---|
| font | 設(shè)置字體和顏色 |
| big | 大號字體 |
| small | 小號字體 |
| i | 斜體 |
| b | 粗體 |
| tt | 等寬字體 |
| br | 換行(行與行之間沒有空行) |
| p | 換行(行與行之間有空行) |
| a | 鏈接 |
| img | 圖像 |
其實TextView支持HTML標(biāo)簽不止這些,后文會帶大家一起看的HTML類的源碼,里面有TextView支持的所有HTML標(biāo)簽。還有一點需要注意的是,不同的標(biāo)簽是有可能會出現(xiàn)相同效果的,例如strong標(biāo)簽和b標(biāo)簽的效果都是字體加粗,這些在大家看到HTML類源碼的時候,就知道會原因。
二、深入的探索
熟悉了用法,我們就要向更深一層探索了。接下來就讓我來帶大家深入TextView源碼,一起揭開TextView富文本顯示的神秘面紗......
Spannable的表示
首先,要想知道TextView的富文本是怎么實現(xiàn)的,我們得先搞明白Android內(nèi)部是怎么表示富文本的,這是Spannable相關(guān)類的繼承體系:

我們之前用的SpannableString類和SpannableStringBuilder類都實現(xiàn)了Spannable接口,setSpan()方法就是在這里聲明的。再看左邊的SpannableString類,它繼承自一個虛類SpannableStringInternal,而我們要找的富文本實現(xiàn)方法就隱藏在這個類中,下面我們就來一探究竟。
void setSpan(Object what, int start, int end, int flags) {
//省略了一些無關(guān)代碼
mSpans[mSpanCount] = what;
mSpanData[mSpanCount * COLUMNS + START] = start;
mSpanData[mSpanCount * COLUMNS + END] = end;
mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
mSpanCount++;
}
還是先進(jìn)到我們最熟悉的setSpan()方法內(nèi)看一下,我們發(fā)現(xiàn)setSpan方法主要更改了三個全局變量的值mSpans, mSpanData和mSpanCount,我們找到這幾個變量的聲明:
private String mText;
private Object[] mSpans;
private int[] mSpanData;
private int mSpanCount;
private static final int START = 0;
private static final int END = 1;
private static final int FLAGS = 2;
private static final int COLUMNS = 3;
其實SpannableStringInternal內(nèi)部存在兩個數(shù)組,一個mSpanData表示樣式的首尾索引和flags,另一個mSpans表示對應(yīng)的樣式。
這個mSpanData的表示比較有意思,它是將三個變量打包存在一起的,取得時候只需要取變量對應(yīng)的偏移地址的值,可以看一下這個mSpanData數(shù)組的表示圖:

而SpannableStringBuilder的就顯得簡單多了,直接將這四個變量分別存放在了四個數(shù)組里,這里就不對它做過多介紹了,有興趣的同學(xué)可以自己探索一下。
富文本的繪制
知道了富文本是怎么表示的,接下來我們就是富文本的繪制了,我們先看一下TextView的onDraw()方法。
protected void onDraw(Canvas canvas) {
//...
if (mLayout == null) {
assumeLayout();
}
Layout layout = mLayout;
//省咯了大量代碼
final int cursorOffsetVertical = voffsetCursor - voffsetText;
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
}
這里就牽扯到了TextView類的構(gòu)成,大家在看TextView源碼的時候會發(fā)現(xiàn)有一萬多行的代碼,其實這是Android為了方便TextView的擴(kuò)展,將很多本不該屬于TextView的代碼也寫在了這里。大家可以看一下EditText的源碼,總共就一百多行,大部分邏輯都是直接交給TextView處理的。而這個mEditor就是用來處理可編輯的TextView的,我們不管它直接看下面,TextView將繪制的細(xì)節(jié)都交給了這個mLayout來做,那這個mLayout又是什么呢?
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
}
//省略一大段代碼,其中包括另外兩個Layout類的實例化,BoringLayout和StaticLayout
return result;
}
在我一番尋找過后,發(fā)現(xiàn)mLayout就是在這里被創(chuàng)建的,我省略了另外兩個Layout子類創(chuàng)建的代碼,BoringLayout和StaticLayout,其實這三個類都是直接調(diào)用了它們父類Layout的draw()方法,而draw()類又調(diào)用了drawText()方法進(jìn)行文本繪制,所以,我們還是直接進(jìn)drawText()方法吧:
Layout:段落格式計算
public void drawText(Canvas canvas, int firstLine, int lastLine) {
TextLine tl = TextLine.obtain();
// Draw the lines, one at a time.
// The baseline is the top of the following line minus the current line's descent.
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
//這里省略了一些段落格式的計算,AlignmentSpan,LeadingMarginSpan都是在這里
Alignment align = paraAlign;
if (align == Alignment.ALIGN_LEFT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE;
} else if (align == Alignment.ALIGN_RIGHT) {
align = (dir == DIR_LEFT_TO_RIGHT) ?
Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL;
}
int x;
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
} else {
x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
} else {
x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
}
} else { // Alignment.ALIGN_CENTER
max = max & ~1;
x = ((right + left - max) >> 1) +
getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
}
}
paint.setHyphenEdit(getHyphen(lineNum));
Directions directions = getLineDirections(lineNum);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab) {
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
paint.setHyphenEdit(0);
}
}
在這里L(fēng)ayout就已經(jīng)計算好了每一行的段落格式,前面空多少、居中還是靠右,而具體的文字顯示樣式則交給了TextLine類來處理。
TextLine:文本繪制
private float handleText(TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl,
Canvas c, float x, int top, int y, int bottom,
FontMetricsInt fmi, boolean needWidth, int offset) {
//...
if (c != null) {
//...
//文字背景
if (wp.bgColor != 0) {
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
wp.setColor(wp.bgColor);
wp.setStyle(Paint.Style.FILL);
c.drawRect(x, top, x + ret, bottom, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
}
if (wp.underlineColor != 0) {
// kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
boolean previousAntiAlias = wp.isAntiAlias();
wp.setStyle(Paint.Style.FILL);
wp.setAntiAlias(true);
wp.setColor(wp.underlineColor);
c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThickness, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
wp.setAntiAlias(previousAntiAlias);
}
//文字繪制,內(nèi)部直接調(diào)用canvas的drawTextRun()
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
x, y + wp.baselineShift);
}
return runIsRtl ? -ret : ret;
}
就在這里TextView把將要顯示的文本繪制到了canvas上,可能又有細(xì)心的同學(xué)發(fā)現(xiàn)了,這TextPaint的各項參數(shù)又是怎么來的呢?這我們就要回到調(diào)用它的方法handleRun()尋找答案了:
private float handleRun(int start, int measureLimit,
int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
int bottom, FontMetricsInt fmi, boolean needWidth) {
//...
for (int i = start, inext; i < measureLimit; i = inext) {
for (int j = i, jnext; j < mlimit; j = jnext) {
//...
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
//...
CharacterStyle span = mCharacterStyleSpanSet.spans[k];
//關(guān)鍵是這句,調(diào)用對應(yīng)Style的updateDrawState()方法,直接設(shè)置TextPaint屬性
span.updateDrawState(wp);
}
x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
}
}
return x - originalX;
}
是不是有種撥開云霧見青天的感覺,至此TextView使用SpannableString繪制富文本的整個流程就呈現(xiàn)在大家眼前了。
使用Html繪制富文本
有同學(xué)就要說了,不是說還有Html類的解析嗎?其實Html內(nèi)部還是將文本轉(zhuǎn)換成了Spannable,原理都是相同的,我這里摘一段大家看一下:
if (tag.equalsIgnoreCase("strong")) {
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("em")) {
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));
}
這也解答了之前說的,為什么不同的標(biāo)簽卻會產(chǎn)生相同效果的疑問。
以上就是這篇文章的全部內(nèi)容了,如有疏漏,請多包涵。