Android 富文本TextView

一、怎么用?

先介紹TextView富文本的使用方法,TextView富文本顯示主要有兩種方式,一個(gè)是使用SpannableString類(lèi),另一種是直接將富文本寫(xiě)成HTML形式。

SpannableString

SpannableString是Android內(nèi)置的專(zhuān)門(mén)處理富文本的類(lèi),基本涵蓋了你能想到的所有富文本表示,字體、顏色、圖片、點(diǎn)擊事件…功能非常強(qiáng)大。話不多說(shuō),直接上代碼:

示例

//設(shè)置Hello World前三個(gè)字符為紅色,背景為藍(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前三個(gè)字符字體為斜體
SpannableString textSpanned2 = new SpannableString("Hello World");
textSpanned2.setSpan(new StyleSpan(Typeface.ITALIC),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text2.setText(textSpanned2);

//設(shè)置Hello World前三個(gè)字符有下劃線
SpannableString textSpanned3 = new SpannableString("Hello World");
textSpanned3.setSpan(new UnderlineSpan(),
        0, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
text3.setText(textSpanned3);

//設(shè)置Hello World前三個(gè)字符有點(diǎn)擊事件
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);
//注意:此時(shí)必須加這一句,不然點(diǎn)擊事件不會(huì)生效
text4.setMovementMethod(LinkMovementMethod.getInstance());
text4.setText(textSpanned4);
Spannable演示

setSpan()

void setSpan (Object what, int start, int end, int flags)
名稱(chēng) 說(shuō)明
what 樣式
start 樣式開(kāi)始的字符索引
end 樣式結(jié)束的字符索引
flags 新插入字符的設(shè)置

flags:

取值 說(shuō)明
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 前后都不包括
Spanned.SPAN_EXCLUSIVE_INCLUSIVE 前面不包括,后面包括
Spanned.SPAN_INCLUSIVE_EXCLUSIVE 前面包括,后面不包括
Spanned.SPAN_INCLUSIVE_INCLUSIVE 前后都包括

這個(gè)flags可能有人不懂,它表示了這個(gè)樣式是否作用在本字符串之前或之后插入的其他字符串上,舉個(gè)例子:

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));
Flags演示
  • text1里,"Hello"的flags是SPAN_EXCLUSIVE_EXCLUSIVE,在它之后插入的"World"顯示正常,無(wú)背景。
  • text2里,"Hello"的flags是SPAN_EXCLUSIVE_INCLUSIVE,它之后插入的"World"的背景變?yōu)樗{(lán)色。
  • 需要注意的是text3,這里的"Hello"與text2相同,而"World"的一部分字符設(shè)置為綠色,顯然這部分字符顯示的是綠色,這說(shuō)明雖然設(shè)置了SPAN_EXCLUSIVE_INCLUSIVE屬性,但只要后面的字符串設(shè)置了同類(lèi)的樣式,還是覆蓋掉flags屬性。

SpannableString和SpannableStringBuilder

在上面的例子中我們用到了SpannableStringBuilder類(lèi),那這個(gè)類(lèi)和SpannableString有什么不同呢?其實(shí)大家只要想想String和StringBuilder區(qū)別就行了,SpannableString在創(chuàng)建的時(shí)候就需要指定好字符串,之后就不能更改了,而SpannableStringBuilder可以使用append()方法,在已有的富文本后添加新的富文本。

HTML

接下來(lái)介紹HTML的用法,其實(shí)HTML使用起來(lái)要比SpannableString簡(jiǎn)潔,我們只需要按照平時(shí)寫(xiě)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));
HTML演示

是不是感覺(jué)比SpannableString簡(jiǎn)單多了,其實(shí)Html類(lèi)內(nèi)部還是在使用Spannable處理,我們會(huì)在后文看到它的實(shí)現(xiàn)方式。

TextView支持的HTML標(biāo)簽
名稱(chēng) 說(shuō)明
font 設(shè)置字體和顏色
big 大號(hào)字體
small 小號(hào)字體
i 斜體
b 粗體
tt 等寬字體
br 換行(行與行之間沒(méi)有空行)
p 換行(行與行之間有空行)
a 鏈接
img 圖像

其實(shí)TextView支持HTML標(biāo)簽不止這些,后文會(huì)帶大家一起看的HTML類(lèi)的源碼,里面有TextView支持的所有HTML標(biāo)簽。還有一點(diǎn)需要注意的是,不同的標(biāo)簽是有可能會(huì)出現(xiàn)相同效果的,例如strong標(biāo)簽和b標(biāo)簽的效果都是字體加粗,這些在大家看到HTML類(lèi)源碼的時(shí)候,就知道會(huì)原因。

二、深入的探索

熟悉了用法,我們就要向更深一層探索了。接下來(lái)就讓我來(lái)帶大家深入TextView源碼,一起揭開(kāi)TextView富文本顯示的神秘面紗......

Spannable的表示

首先,要想知道TextView的富文本是怎么實(shí)現(xiàn)的,我們得先搞明白Android內(nèi)部是怎么表示富文本的,這是Spannable相關(guān)類(lèi)的繼承體系:


Spannable繼承體系

我們之前用的SpannableString類(lèi)和SpannableStringBuilder類(lèi)都實(shí)現(xiàn)了Spannable接口,setSpan()方法就是在這里聲明的。再看左邊的SpannableString類(lèi),它繼承自一個(gè)虛類(lèi)SpannableStringInternal,而我們要找的富文本實(shí)現(xiàn)方法就隱藏在這個(gè)類(lèi)中,下面我們就來(lái)一探究竟。

void setSpan(Object what, int start, int end, int flags) {

    //省略了一些無(wú)關(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方法主要更改了三個(gè)全局變量的值mSpans, mSpanData和mSpanCount,我們找到這幾個(gè)變量的聲明:

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;

其實(shí)SpannableStringInternal內(nèi)部存在兩個(gè)數(shù)組,一個(gè)mSpanData表示樣式的首尾索引和flags,另一個(gè)mSpans表示對(duì)應(yīng)的樣式。
這個(gè)mSpanData的表示比較有意思,它是將三個(gè)變量打包存在一起的,取得時(shí)候只需要取變量對(duì)應(yīng)的偏移地址的值,可以看一下這個(gè)mSpanData數(shù)組的表示圖:


mSpanData構(gòu)成

而SpannableStringBuilder的就顯得簡(jiǎn)單多了,直接將這四個(gè)變量分別存放在了四個(gè)數(shù)組里,這里就不對(duì)它做過(guò)多介紹了,有興趣的同學(xué)可以自己探索一下。

富文本的繪制

知道了富文本是怎么表示的,接下來(lái)我們就是富文本的繪制了,我們先看一下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類(lèi)的構(gòu)成,大家在看TextView源碼的時(shí)候會(huì)發(fā)現(xiàn)有一萬(wàn)多行的代碼,其實(shí)這是Android為了方便TextView的擴(kuò)展,將很多本不該屬于TextView的代碼也寫(xiě)在了這里。大家可以看一下EditText的源碼,總共就一百多行,大部分邏輯都是直接交給TextView處理的。而這個(gè)mEditor就是用來(lái)處理可編輯的TextView的,我們不管它直接看下面,TextView將繪制的細(xì)節(jié)都交給了這個(gè)mLayout來(lái)做,那這個(gè)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);
    }

    //省略一大段代碼,其中包括另外兩個(gè)Layout類(lèi)的實(shí)例化,BoringLayout和StaticLayout

    return result;
}

在我一番尋找過(guò)后,發(fā)現(xiàn)mLayout就是在這里被創(chuàng)建的,我省略了另外兩個(gè)Layout子類(lèi)創(chuàng)建的代碼,BoringLayout和StaticLayout,其實(shí)這三個(gè)類(lèi)都是直接調(diào)用了它們父類(lèi)Layout的draw()方法,而draw()類(lèi)又調(diào)用了drawText()方法進(jìn)行文本繪制,所以,我們還是直接進(jìn)drawText()方法吧:

Layout:段落格式計(jì)算

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++) {

        //這里省略了一些段落格式的計(jì)算,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)計(jì)算好了每一行的段落格式,前面空多少、居中還是靠右,而具體的文字顯示樣式則交給了TextLine類(lèi)來(lái)處理。

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的各項(xiàng)參數(shù)又是怎么來(lái)的呢?這我們就要回到調(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)用對(duì)應(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;
}

是不是有種撥開(kāi)云霧見(jiàn)青天的感覺(jué),至此TextView使用SpannableString繪制富文本的整個(gè)流程就呈現(xiàn)在大家眼前了。

使用Html繪制富文本

有同學(xué)就要說(shuō)了,不是說(shuō)還有Html類(lèi)的解析嗎?其實(shí)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));
}

這也解答了之前說(shuō)的,為什么不同的標(biāo)簽卻會(huì)產(chǎn)生相同效果的疑問(wèn)。

以上就是這篇文章的全部?jī)?nèi)容了,如有疏漏,請(qǐng)多包涵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容