Android 千變?nèi)f化 TextView:神奇的 SpannableString

之前寫過一篇SpannableString的文章,最近搬出來統(tǒng)一放在簡書上。

前言

TextView 可以說是 Android 中最簡單、最常見的文字控件了,幾乎每個頁面都有 TextView 的身影,絕大多數(shù)情況我們用 TextView 只是單純地顯示一個文本,但是 TextView 的功能遠(yuǎn)遠(yuǎn)不止如此哦,簡單的 TextView 也能千變?nèi)f化顯示出各種效果,這一切都要歸功于 SpannableString。

TextView 和 SpannableString 一起使用具體有哪些神奇的地方呢?本場 Chat 將全面地介紹 SpannableString 的用法,讓你的 TextView 不再簡單。

SpannableString

在 Android 中,常規(guī)的字符串類就是 String 或者 Charsequence,String 用的最多,有些人可能對 Charsequence 都有點陌生,EditText 的 getText() 返回的就是 Charsequence 對象。但是今天我們要介紹的 SpannableString 就是另一種更強大的字符串類。

Spannable 是什么意思?英語詞典上還真不太好查,我自己的理解的意思是:可測量、可塑造的,所以 SpannableString 就是一種可測量可塑造的字符串。

1)默認(rèn) TextView 樣式

默認(rèn) TextView 樣式我們再熟悉不過了,看下截圖,沒啥好說的。

enter image description here

2)自定義字體

SpannableString 可以給 TextView 設(shè)置自定義字體樣式,并且可以指定某幾個字,其實 SpannableString 幾乎所有的屬性可可以指定到具體某幾個字。

SpannableString ss = new SpannableString(txCustomTypeface.getText());
ss.setSpan(new TypefaceSpan("sans-serif"), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txCustomTypeface.setText(ss);

這里用到了一個新的類:TypefaceSpan,它就是用來設(shè)置字體樣式的,參數(shù)有 5 個可選值:default、default-bold、monospace、serif、sans-serif。后面的 2 和 4 是需要生效的起始位置和結(jié)束位置。

enter image description here

在這個例子中,我們把 2 - 4 的文字設(shè)置成了 sans-serif 樣式,但是竟然看不出任何差別。不過也不必奇怪,這些字體樣式之間的差異確實非常小,根據(jù)一篇專業(yè)的字體研究報告稱,sans 字體適合正文內(nèi)容文字,能長時間集中視覺注意力,而 sans-serif 適合標(biāo)題文字,能快速抓住注意力,但不適宜長時間閱讀??傊@之間的差別是比較專業(yè)的,在這個例子中確實看不出多大區(qū)別。

3)絕對字體和相對字體

SpannableString 可以動態(tài)地改變字體大小,并且支持絕對大小和相對大小兩種模式。

絕對大小
SpannableString ss = new SpannableString(txAbsoluteSize.getText());
ss.setSpan(new AbsoluteSizeSpan(12, true), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txAbsoluteSize.setText(ss);

enter image description here

圖中可以看到中間兩個字變小了,AbsoluteSizeSpan 就是構(gòu)建絕對大小的類,它有兩個參數(shù),第一個表示字體大小,第二個表示是否使用 DIP,false 的話單位就是 px,true 的話單位就是 dp。

相對大小
SpannableString ss = new SpannableString(txRelativeSize.getText());
ss.setSpan(new RelativeSizeSpan(1.5f), 2, 4, SPAN_EXCLUSIVE_EXCLUSIVE);
txRelativeSize.setText(ss);

enter image description here

相對字體大小就簡單一些了,只需要傳入一個字體相對大小,比如我們傳入了 1.5,中間兩個字就變成了原始字體的 1.5 倍大。

4)前景色和背景色

其實對于 TextView 來說,前景色就是 textColor,背景色就是 background。你可能會覺得那為什么要用 SpannableString 來做呢,直接用 textColor 和 background 不就可以了嗎?但是 textColor 和 background 只能對 textView 整體生效,而 SpannableString 可以動態(tài)給不同位置的文字設(shè)置不同顏色。

前景色
SpannableString ss = new SpannableString(txForegroundColor.getText());
ss.setSpan(new ForegroundColorSpan(Color.BLUE), 0, txForegroundColor.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txForegroundColor.setText(ss);

enter image description here
背景色
SpannableString ss = new SpannableString(txBackgroundColor.getText());
ss.setSpan(new BackgroundColorSpan(Color.LTGRAY), 0, 
    txBackgroundColor.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txBackgroundColor.setText(ss);

enter image description here

5)字體的加粗和傾斜

這里和大多數(shù)編輯器一樣,支持三種:粗體、斜體、粗斜體。

對應(yīng)的常量是:Typeface.BOLD、Typeface.ITALIC、Typeface.BOLD_ITALIC。

SpannableString ss = new SpannableString(txBord.getText());
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, txBord.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txBord.setText(ss);

enter image description here

6)刪除線和下劃線

刪除線和下劃線是兩種常用文本標(biāo)記符號,SpannableString 當(dāng)然也是支持的。設(shè)置刪除線和下劃線很簡單,只要指定起始位置和結(jié)束位置即可,下面直接看代碼和效果圖吧。

刪除線

刪除線用到的類是 StrikethroughSpan,沒有參數(shù)。

SpannableString ss = new SpannableString(txDeleteLine.getText());
ss.setSpan(new StrikethroughSpan(), 0, txDeleteLine.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txDeleteLine.setText(ss);

enter image description here
下劃線

下劃線用到的類是 UnderlineSpan,沒有參數(shù)。

SpannableString ss = new SpannableString(txUnderLine.getText());
ss.setSpan(new UnderlineSpan(), 0, txUnderLine.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txUnderLine.setText(ss);

enter image description here

7)文字的上標(biāo)和下標(biāo)

這個在實際開發(fā)中不常用,但是卻很重要,因為萬一遇到這種需求要自己實現(xiàn)的話還挺麻煩的。SpannableString 實現(xiàn)起來就很簡單了。

SpannableString ss = new SpannableString(txSubSuperScript.getText());
ss.setSpan(new SuperscriptSpan(), 2, 3, SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new SubscriptSpan(), 5, 6, SPAN_EXCLUSIVE_EXCLUSIVE);
txSubSuperScript.setText(ss);

enter image description here

8)6 種超鏈接形式

我記得我實習(xí)那會遇到一個需求要實現(xiàn)一個 TextView 中超鏈接的功能,那時候我還不知道 SpannableString,想了各種辦法,頭都大了。

SpannableString 支持 6 中超鏈接形式,分別是: 電話超鏈接、郵件超鏈接、網(wǎng)址超鏈接、短信超鏈接、彩信超鏈接、地圖超鏈接。

a.電話超鏈接

這里又涉及到了一個新的類:URLSpan,實際上6種超鏈接都是使用 URLSpan 構(gòu)建的,只是構(gòu)造函數(shù)傳入的鏈接格式不一樣, 電話超鏈接傳入的是 tel: 開頭,后面接要撥打的電話號碼,點擊后就會自動跳轉(zhuǎn)撥打電話。

SpannableString ss = new SpannableString(txTelUrl.getText());
ss.setSpan(new URLSpan("tel:02512345678"), 0, txTelUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txTelUrl.setText(ss);
txTelUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
b.郵件超鏈接

郵件超鏈接是以 mailto: 開頭,后面接郵箱地址。點擊后就會自動跳轉(zhuǎn)郵件 app。

SpannableString ss = new SpannableString(txMailUrl.getText());
ss.setSpan(new URLSpan("mailto:xxx@google.com"), 0, txMailUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txMailUrl.setText(ss);
txMailUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here

如果你的手機里存在多個郵件 app,需要選擇一個。

enter image description here
c.網(wǎng)址超鏈接

網(wǎng)址超鏈接是以 http:// 或 https:// 開頭,后面接網(wǎng)址,點擊后跳轉(zhuǎn)瀏覽器 app,同樣如果有多個瀏覽器,需要作出選擇。

SpannableString ss = new SpannableString(txWebUrl.getText());
ss.setSpan(new URLSpan("http://www.baidu.com"), 0, txWebUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txWebUrl.setText(ss);
txWebUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
d.短信超鏈接

短信超鏈接是以 sms: 開頭,后面接手機號碼,點擊后跳轉(zhuǎn)系統(tǒng)短信 app。

SpannableString ss = new SpannableString(txSmsUrl.getText());
ss.setSpan(new URLSpan("sms:02512345678"), 0, txSmsUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txSmsUrl.setText(ss);
txSmsUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
e.彩信超鏈接

彩信超鏈接是以 mms: 開頭,后面接手機號碼,點擊永陽跳轉(zhuǎn)系統(tǒng)短信 app。

SpannableString ss = new SpannableString(txMmsUrl.getText());
ss.setSpan(new URLSpan("mms:02512345678"), 0, txMmsUrl.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txMmsUrl.setText(ss);
txMmsUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here
f.地圖超鏈接

地圖超鏈接以 geo: 開頭,后面接經(jīng)緯度,點擊后跳轉(zhuǎn)地圖 app。

SpannableString ss = new SpannableString(txGeoUrl.getText());
ss.setSpan(new URLSpan("geo:30.123456,-50.024456"), 0, 
    txGeoUrl.getText().length(), SPAN_EXCLUSIVE_EXCLUSIVE);
txGeoUrl.setText(ss);
txGeoUrl.setMovementMethod(LinkMovementMethod.getInstance());

enter image description here

如果你的手機有多個地圖 app,需要選擇一個默認(rèn) app。

enter image description here

9)添加項目符號

關(guān)于這一點,客觀地說用處不大,SpannableString 雖然支持設(shè)置項目符號,但是實際開發(fā)中基本不會用,如果是頁面中的欄位,我們肯定會用小 icon 實現(xiàn)項目符號,如果是 H5,那就是 HTML 的標(biāo)簽實現(xiàn)。

BulletSpan 類用于構(gòu)建項目符號,第一個參數(shù)是項目符號所占的寬度,第二個參數(shù)是項目符號的顏色。

SpannableString ss = new SpannableString(txBullte.getText());
ss16.setSpan(new BulletSpan(20, Color.RED), 0, txBullte.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txBullte.setText(ss);

enter image description here

10)文字的橫向和縱向拉伸

一般我們要改變字體大小,都是設(shè)置 textSize 屬性,這個屬性是文字整體等比例放大縮小,那如果我只想文字橫向拉伸呢?這時候就要用到 SpannableString 了。

SpannableString ss = new SpannableString(txScaleX.getText());
ss.setSpan(new ScaleXSpan(2.5f), 0, txScaleX.getText().length(), 
    SPAN_EXCLUSIVE_EXCLUSIVE);
txScaleX.setText(ss);

enter image description here

ScaleXSpan 類用于指定橫向拉伸的比例,我們傳 2.5 表示橫向拉伸為原來的 2.5 倍。

有了橫向拉伸,自然我們會想縱向拉伸,不好意思,不支持。因為縱向的高度就得用 textSize 設(shè)置。

11)ColorStateList

這個東西我很少發(fā)現(xiàn)有人用,可能是因為不知道有這個類,也可能是因為這個用起來太麻煩。但不代表這個東西沒用。

大家有沒有遇到過這樣的場景,一個 Button,默認(rèn)灰色背景,黑色文字,按下后,背景要變成黑色,這個需求很常見,但是你有可能遇到這樣的場景。

enter image description here

本來文字就是黑色,按下后背景變成黑色,文字就看不見了,背景顏色和文字顏色的對比度太低了甚至為 0,導(dǎo)致文字不可見。

我們希望正常狀態(tài)下背景灰色,文字黑色,按下狀態(tài)背景變成黑色,文字變成白色。這時候就要用到 ColorStateList。

首先像以前一樣定義一個 drawable,button_text.xml

<?xml version="1.0" encoding="utf-8"?>
<selector 
    xmlns:android="http://schemas.android.com/apk/res/android" 
    android:enterFadeDuration="300" 
    android:exitFadeDuration="300">

    <item android:state_pressed="true" android:color="#ffffff"/>
    <item android:color="#000000"/>
</selector>

然后解析 xml,構(gòu)建 ColorStateList 并設(shè)置給 textView,效果就實現(xiàn)了。

ColorStateList csl = null;
try {
    =XmlResourceParser xrp = getResources().getXml(R.drawable.button_text);
    csl = ColorStateList.createFromXml(getResources(), xrp);
} catch (XmlPullParserException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
btn.setTextColor(csl);

enter image description here

實戰(zhàn):表情文字

下面我們來做一個稍有難度的小項目:表情文字。 其效果就和常規(guī)的聊天軟件一樣,可以混合輸入表情和文字,并且可以顯示在聊天記錄中。

enter image description here

看上去效果還不錯,表情和文字稍微有點不對齊(偏下),還可以再優(yōu)化下,后面代碼分析也會說到。文字和表情可以混排,輸入框中輸入的表情和聊天列表中顯示一致,基本功能都實現(xiàn)了。下面就來看下是怎么實現(xiàn)的吧。

1)分析

整個過程可以分成兩步,第一步是讓輸入框 EditText 可以輸入表情,第二步是把輸入框輸入的表情顯示到 TextView 上。

2)準(zhǔn)備表情資源

我在網(wǎng)上下載了一批常用的表情圖片,放在 drawable - xxhdpi 目錄下:

enter image description here

3)給表情編碼

我們在 assets 目錄下新建一個文件 emotion.xml,我們把每一個表情定義為一個 emotion,有 code 和 name 兩個屬性,name 就是表情圖片的文件名。

<?xml version="1.0" encoding="utf-8"?>
<emotions>
    <emotion>
        <code><![CDATA[[em:1:]]]></code>
        <name>f001</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:2:]]]></code>
        <name>f002</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:3:]]]></code>
        <name>f003</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:4:]]]></code>
        <name>f004</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:5:]]]></code>
        <name>f005</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:6:]]]></code>
        <name>f006</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:7:]]]></code>
        <name>f007</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:8:]]]></code>
        <name>f008</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:9:]]]></code>
        <name>f009</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:10:]]]></code>
        <name>f010</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:11:]]]></code>
        <name>f011</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:12:]]]></code>
        <name>f012</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:13:]]]></code>
        <name>f013</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:14:]]]></code>
        <name>f014</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:15:]]]></code>
        <name>f015</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:16:]]]></code>
        <name>f016</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:17:]]]></code>
        <name>f017</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:18:]]]></code>
        <name>f018</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:19:]]]></code>
        <name>f019</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:20:]]]></code>
        <name>f020</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:21:]]]></code>
        <name>f021</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:22:]]]></code>
        <name>f022</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:23:]]]></code>
        <name>f023</name>
    </emotion>
    <emotion>
        <code><![CDATA[[em:24:]]]></code>
        <name>f024</name>
    </emotion>
</emotions>

4)解析 emotion.xml

xml 只是配置,最終肯定要解析成 java bean,下面是我的解析過程。

當(dāng)然你也可以用 json 編碼 emotion,然后解析 json,可能會比解析 xml 要簡單些

public static List<Emotion> getEmotions(InputStream inputStream) {
    XmlPullParser parser = Xml.newPullParser();
    int eventType = 0;
    List<Emotion> emotions = null;
    Emotion emotion = null;
    try {
        parser.setInput(inputStream, "UTF-8");
        eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) {

            switch (eventType) {
            case XmlPullParser.START_DOCUMENT:

                emotions = new ArrayList<Emotion>();
                break;
            case XmlPullParser.START_TAG:
                if ("emotion".equals(parser.getName())) {
                    emotion = new Emotion();

                } else if ("code".equals(parser.getName())) {
                    emotion.setCode(parser.nextText());
                } else if ("name".equals(parser.getName())) {
                    emotion.setName(parser.nextText());
                }
                break;
            case XmlPullParser.END_TAG:
                if ("emotion".equals(parser.getName())) {
                    emotions.add(emotion);
                    emotion = null;
                }
                break;
            default:
                break;
            }
            eventType = parser.next();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return emotions;
}

5)顯示表情

拿到了表情列表,顯示出來就簡單了,我們隨便用 GridView 或者 RecyclerView 都可以,太基礎(chǔ)了,這部分代碼就不放出來了,直接看下效果圖吧。

enter image description here

6)輸入表情

哎,關(guān)鍵的地方來了,怎么把表情輸入到 EditText 中呢?

我們這篇文章講的是 SpannableString,那當(dāng)然是用 SpannableString 做。

SpannableString 除了可以像前面那樣把文字變大變小變長變色,還可以把一部分文字變成圖片,承載圖片的是 Drawable 對象,而實現(xiàn)這個效果的就是 ImageSpan。

看下基本使用方法

SpannableString ss = new SpannableString(str);
ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM);
ss.setSpan(span, 0, str.length(), SPAN_EXCLUSIVE_EXCLUSIVE);

ImageSpan 的構(gòu)造函數(shù)要傳 2 個參數(shù),drawable 對象和對齊方式,這里的對齊方式就是表情和文字的對齊方式,只有兩個選項:

ALIGN_ BASELINE 和 ALIGN_ BOTTOM,我這里選擇的是 ALIGN_BOTTOM,所以表情相對文字會偏下。

這樣設(shè)置后,字符串 str 就和 drawable 對象對應(yīng)上了,在顯示時會顯示 drawable,但是調(diào)用 editText.getText() 得到的還是字符串。

弄懂了這個原理,再看下面代碼就簡單多了。

@Override
public void onItemClick(AdapterView<?> p, View v, int position, long id) {
    Emotion emotion = emotions.get(position);
    int cursor = etInput.getSelectionStart();
    Field f;
    try {
        f = (Field) R.drawable.class.getDeclaredField(emotion.getName());
        int j = f.getInt(R.drawable.class);
        Drawable d = getResources().getDrawable(j);
        int textSize = (int)etInput.getTextSize();
        d.setBounds(0, 0, textSize, textSize);

        String str = null;
        int pos = position + 1;
        if (pos < 10) {
            str = "f00" + pos;
        } else if (pos < 100) {
            str = "f0" + pos;
        } else {
            str = "f" + pos;
        }
        SpannableString ss = new SpannableString(str);
        ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM);
        ss.setSpan(span, 0, str.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        etInput.getText().insert(cursor, ss);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

上述代碼可簡單分析成以下步驟:
(1)根據(jù)點擊位置,獲取到該位置的 Emotion 對象。
(2)根據(jù) emotion 的 name,通過反射的方式獲取到 Drawable 對象。
(3)根據(jù) EditText 的 textSize 設(shè)置 drawable 的大小,為了看上去表情和文字是協(xié)調(diào)的,我直接把 drawable 的寬高設(shè)置成了textSize。
(4)構(gòu)建 ImageSpan 和 SpannableString,把 drawable 和字符串 str 對應(yīng)起來。
(5)把 SpannableString 插入到 EditText 當(dāng)前光標(biāo)位置。

這樣解釋是不是太簡單了,可是代碼確實很簡單啊。至此,我們算是實現(xiàn)了第一步:在 EditText 中輸入表情,接下來就要實現(xiàn)第二步,把輸入的表情顯示在聊天記錄中。

7)把輸入的表情顯示在聊天列表

我們既然已經(jīng)把表情輸入到 EditText 了,顯示到 TextView 還不簡單,直接把 SpannableString 設(shè)置給 TextView 不就行了嗎?

在 demo 中是可以,但是在實際項目中不行。實際項目中輸入的內(nèi)容是要轉(zhuǎn)成 String 傳輸?shù)?,再發(fā)給客戶端,客戶端接收到消息后再解析顯示。所以這就需要再執(zhí)行一次構(gòu)建 SpannableString 的操作,具體代碼如下:

(1)首先獲取 EditText 輸入的內(nèi)容,然后經(jīng)過一個 getExpressionString 方法轉(zhuǎn)成 SpannableString,然后添加到 adapter 中刷新聊天列表,最后清空輸入框。

public void onSendClick() {
    String receiveStr = etInput.getText().toString();
    SpannableString ss= getExpressionString(this, receiveStr, textSize);
    messages.add(ss);
    adapter.notifyDataSetChanged();
    lvMsg.setSelection(messages.size() - 1);
    etInput.setText(null);
}

(2)那么重點就是 getExpressionString 方法了,這個方法構(gòu)建一個 SpannableString 和一個正則匹配模式,接著又調(diào)用了 dealExpression 方法。

public static final String PATTEN_STR = "f0[0-9]{2}|f10[0-7]";

public SpannableString getExpressionString(Context context, String str, 
        int textSize) {
    SpannableString ss = new SpannableString(str);
    Pattern sinaPatten = Pattern.compile(PATTEN_STR, Pattern.CASE_INSENSITIVE);
    try {
        dealExpression(context, ss, textSize, sinaPatten, 0);
    } catch (Exception e) {
        Log.e("dealExpression", e.getMessage());
    }
    return ss;
}

(3)真正的重點來了,這個方法中利用正則匹配模式,找到輸入內(nèi)容中每一條符合正則的子字符串,也就是表情編碼的字符串,然后像之前那樣通過反射獲取 Drawable,構(gòu)建 SpannableString 把 Drawable 和 String 對應(yīng)起來。

(此部分代碼和之前是一樣的)

public void dealExpression(Context context, SpannableString ss, 
        int textSize, Pattern patten, int start) throws Exception {
    Matcher matcher = patten.matcher(ss);
    while (matcher.find()) {
        String key = matcher.group();
        if (matcher.start() < start) {
            continue;
        }
        Field field = R.drawable.class.getDeclaredField(key);
        int resId = field.getInt(R.drawable.class);
        if (resId != 0) {
            Drawable d = context.getResources().getDrawable(resId);
            d.setBounds(0, 0, textSize, textSize);
            ImageSpan imageSpan = new ImageSpan(d);
            int end = matcher.start() + key.length();
            ss.setSpan(imageSpan, matcher.start(), end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
            if (end < ss.length()) {
                dealExpression(context, ss, textSize, patten, end);
            }
            break;
        }
    }
}

看到這你明白了嗎?整個過程就是操作 SpannableString 的過程,SpannableString 內(nèi)部通過 ImageSpan 把字符串和 Drawable 對應(yīng)起來,在顯示的時候表現(xiàn)為 Drawable,在 getText 時表現(xiàn)為普通 String。

就是這么簡單,以前可能覺得表情文字是很神奇的存在,現(xiàn)在是不是覺得就是紙老虎。

大工告成!至此,整個實現(xiàn)的邏輯就講完了,但是我的工程中遠(yuǎn)不止這些,還有很多邊緣性的功能,但核心的東西都講了。

最后,我把完整的工程代碼放出來,需要的朋友下載吧。
https://gitee.com/alexandor/EmotionText

好了,以上就是本期 Chat 的全部內(nèi)容,感謝大家的支持,如有錯誤或不當(dāng)之處還請指出。

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

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

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