簡介
自定義的表情輸入鍵盤在很多應(yīng)用中都會(huì)有用到,譬如微信、QQ 等社交聊天軟件中更是不可缺少的部分。本文將解析一下個(gè)人的自定義表情輸入控件庫 PandaEmoView 的實(shí)現(xiàn)和使用。
該庫具有以下特點(diǎn):
- 支持 emoji 表情圖片
- 支持 gif 動(dòng)態(tài)表情輸入顯示
- 支持單張貼圖表情(與微信收藏表情一致)
- 支持題圖表情庫的添加刪除
效果圖:

快速使用
引入庫
compile 'com.pandaq:PandaEmoView:1.0.0'
表情資源及配置文件
- 默認(rèn)的 emoji 和 gif 表情以及他們的配置文件是放在開發(fā)包 assets 目錄下的,若表情比較多比較大也可自行修改源碼在 APP 啟動(dòng)時(shí)從服務(wù)器下載。

- emoji 表情配置文件

- 非自定義 sticker 配置文件(自定義 sticker 是沒有配置文件的)

具體使用規(guī)則
與表情輸入控件相關(guān)的 EditText 必須使用 PandaEditText
PandaEditText 只是重寫了 onKeyPreIme() 獲取按返回鍵的通知,繼承自 EditText 的控件可繼承 PandaEditText 自定義
1.應(yīng)用 Application 中進(jìn)行全局參數(shù)配置
private void configPandaEmoView() {
new PandaEmoManager.Builder()
.with(getApplicationContext()) // 傳遞 Context
.configFileName("emoji.xml")// 配置文件名稱
.emoticonDir("face") // asset 下存放表情的目錄路徑(asset——> configFileName 之間的路徑,結(jié)尾不帶斜杠)
.sourceDir("images") // 存放 emoji 表情資源文件夾路徑(emoticonDir 圖片資源之間的路徑,結(jié)尾不帶斜杠)
.showAddTab(true)//tab欄是否顯示添加按鈕
.showStickers(true)//tab欄是否顯示貼圖切換按鍵
.showSetTab(true)//tab欄是否顯示設(shè)置按鈕
.defaultBounds(30)//emoji 表情顯示出來的寬高
.cacheSize(1024)//加載資源到內(nèi)存時(shí) LruCache 緩存大小
.defaultTabIcon(R.drawable.ic_default)//emoji表情Tab欄圖標(biāo)
.emojiColumn(7)//單頁顯示表情的列數(shù)
.emojiRow(3)//單頁顯示表情的行數(shù)
.stickerRow(2)//單頁顯示貼圖表情的行數(shù)
.stickerColumn(4)//單頁顯示貼圖表情的列數(shù)
.maxCustomStickers(30)//允許添加的收藏表情數(shù)
.imageLoader(new IImageLoader() {
@Override
public void displayImage(String path, ImageView imageView) { // 加載貼圖表情的圖片加載接口
Picasso.with(getApplicationContext())
.load(path)
.fit()
.centerCrop()
.into(imageView);
}
})
.build(); //構(gòu)建 PandaEmoManager 單利
}
2.使用此控件的 Activity 在 manifest 文件中配置
// 這句是一定要加上的。
android:windowSoftInputMode="adjustResize"
3.使用此控件的界面 xml 文件規(guī)則
布局規(guī)則如下圖,lockView 即是我們正常顯示內(nèi)容的 View 它與表情輸入控件 PandaEmoView 屬于同一層級,父布局必須為縱向線性布局,且設(shè)置 lockView 權(quán)重為 1 ,PandaEmoView 高度包裹內(nèi)容即可

4.使用控件的 Activity Java 代碼設(shè)置
//界面控件初始化后 .attachEditText()綁定輸入控件
//初始化 KeyBoardManager,PandaEmoView.attachEditText() 必須在后調(diào)用
主要使用類及公有方法概覽
PandaEmoEditText
- 表情輸入框繼承自
EditText只對onKeyIme()進(jìn)行復(fù)寫用于監(jiān)聽輸入鍵盤或者軟鍵盤的彈出與關(guān)閉
PandaEmoView
- 表情輸入控件 View 繼承自
RelativeLayout
| 方法 | 返回值 | 參數(shù) | 描述 |
|---|---|---|---|
| attachEditText(PandaEmoEditText input) | void | 表情輸入控件(如使用自定義 EditText 可直接繼承重寫) | 綁定輸入控件 |
| getAttachEditText() | void | 獲取當(dāng)前表情控件綁定的輸入框控件 | |
| reloadEmos | void | position 重載表情控件數(shù)據(jù)后默認(rèn)選中 Tab 的 position | 添加或者刪除表情數(shù)據(jù)后重載刷新表情控件 |
PandaEmoManager
-
PandaEmoManager為核心配置類,表情控件的各種參數(shù)都通過此類的構(gòu)造器進(jìn)行配置
| 屬性 | 類型 | 描述 | 默認(rèn)值 |
|---|---|---|---|
| EMOT_DIR | String | 默認(rèn)表情圖在 assets 中的路徑 (assets 目錄到配置 xml 文件之間的部分的路徑,demo 中的 face) | face |
| SOURCE_DIR | String | EMOT_DIR 目錄下存放圖片資源的文件夾路徑 (demo 中的 images) | source_default |
| STICKER_PATH | String | Sticker貼圖包的存放目錄,該目錄下的每一個(gè)文件目錄都為一個(gè)貼圖包 | /data/data/< package name>/files/sticker |
| CACHE_MAX_SIZE | int | 加載表情 LruCache 緩存大小 | 1024 |
| DEFAULT_EMO_BOUNDS_DP | int | 表情圖顯示大小(非貼圖表情) | 30dp |
| defaultIcon | int | 表情 Tab 資源文件名 | R.drawable.ic_default |
| mContext | Context | 上下文 | null |
| mConfigFile | String | EMOT_DIR 目錄下的 emoji 配置文件名稱 | emoji_default.xml |
| mIImageLoader | IImageLoader | Sticker 圖片加載器接口,加載方式外部傳入 | null |
| MAX_CUSTOM_STICKER | int | 最大添加的自定義貼圖表情數(shù) | 30 |
| EMOJI_ROW | int | emoji 表情單頁行數(shù) | 3 |
| EMOJI_COLUMN | int | emoji 表情單頁列數(shù) | 7 |
| STICKER_ROW | int | sticker 表情單頁行數(shù) | 2 |
| STICKER_COLUMN | int | sticker 表情單頁列數(shù) | 4 |
| showAddButton | boolean | tab 欄是否顯示添加按鈕 | ture |
| showSetButton | boolean | tab 欄是否顯示設(shè)置按鈕 | ture |
| showStickers | boolean | tab 欄是否顯示貼圖按鈕(所以 sticker) | ture |
| 方法 | 返回值 | 參數(shù) | 描述 |
|---|---|---|---|
| init() | void | 無參 | 初始化 拼接 Sticker 路徑及創(chuàng)建自定義貼圖 文件夾 (STICKER_PATH + "/selfSticker" ) |
| makePattern() | Pattern | 無參 | 創(chuàng)建 emoji 正則匹配器 |
剩余方法都為屬性值的 getter() setter() 不在贅述。
PandaEmoManager.Builder
-
PandaEmoManager的構(gòu)造器類,屬性及方法都與 PandaEmoManager 一一對應(yīng);
KeyBoardManager
-
KeyBoardManager為輸入法軟鍵盤與表情輸入控件協(xié)調(diào)管理類
| 屬性 | 類型 | 描述 | 默認(rèn)值 |
|---|---|---|---|
| SHARE_PREFERENCE_NAME | String | 用于存儲(chǔ)鍵盤高度的 SP 的名字 | "EmotionKeyBoard" |
| SHARE_PREFERENCE_SOFT_INPUT_HEIGHT | String | 用于存儲(chǔ)鍵盤高度的 key | "EmotionKeyBoard" |
| mActivity | Activity | 控件依附的 Activity 界面 | null |
| mEmotionView | PandaEmoView | 當(dāng)前管理的表情輸入控件 | null |
| interceptBackPress | boolean | 是否攔截返回鍵 | false |
| lockView | View | 鎖定高度的 View(即同一線性父布局中,表情控件之外的布局視圖) | null |
| mOnEmotionButtonOnClickListener | OnEmotionButtonOnClickListener | 表情顯示控制按鈕監(jiān)聽 | null |
| mOnInputShowListener | OnInputShowListener | 監(jiān)聽輸入欄的彈出與關(guān)閉 | null |
| 方法 | 返回值 | 參數(shù) | 描述 |
|---|---|---|---|
| with() | KeyBoardManager | Activity : 當(dāng)前輸入控件依附的 Activity | 初始化 KeyBoardManager 創(chuàng)建單例 |
| bindToLockContent() | KeyBoardManager | lockView:切換需要鎖定高度的 View | 賦值給內(nèi)部屬性 lockView |
| bindToEmotionButton() | KeyBoardManager | View...: 多個(gè) View 參數(shù),切換控制按鈕 | 為輸入控件綁定控制按鈕 |
| setEmotionView() | KeyBoardManager | PandaEmoView: 被管理的表情控件 | 綁定當(dāng)前管理的輸入控件(綁定的控件必須在此之前調(diào)用 PandaEmoView.attachEditText() 否則內(nèi)部將會(huì)空指針) |
| interceptBackPress() | boolean | null | 在 Activity 的 backPressd() 中檢查是否需要攔截返回鍵關(guān)閉輸入欄而不是退出界面 |
| hideInputLayout() | void | null | 供外部調(diào)用關(guān)閉輸入欄(不能未初始化直接調(diào)用) |
| showInputLayout() | void | null | 供外部調(diào)用顯示輸入欄(不能未初始化直接調(diào)用) |
EmoticonManager
-
EmoticonManager為 emoji 表情加載管理類,此類提供方法將資源文件根據(jù)配置文件加載進(jìn)內(nèi)存,方法大多數(shù)為私有方法,源碼中可查看注釋。
StickerManager
-
StickerManager為 sticker 表情加載管理類,此類提供方法將資源文件根據(jù)配置文件加載進(jìn)內(nèi)存,與EmoticonManager類似
PandaEmoTranslator
-
PandaEmoTranslator為 emoji 表情 [文字] 轉(zhuǎn)表情的轉(zhuǎn)換工具類
| 方法 | 返回值 | 參數(shù) | 描述 |
|---|---|---|---|
| getInstance() | PandaEmoTranslator | null | 獲取文本表情轉(zhuǎn)換器單例 |
| setMaxGifPerView() | void | maxGifPerView: 每個(gè) TextView 控件最多顯示動(dòng)態(tài)表情的個(gè)數(shù) | 設(shè)置每個(gè) TextView 控件最多顯示動(dòng)態(tài)表情的個(gè)數(shù),超過此數(shù)全部顯示為靜態(tài)表情 |
| getMaxGifPerView() | int | null | 獲取每個(gè) TextView 控件最多顯示動(dòng)態(tài)表情的個(gè)數(shù) |
| makeGifSpannable() | SpannableString | classTag:PandaEmoView 依附的 ActivityTag(推薦使用 Activity.getLocalClassName());value : 待替換的文本 ;gif 運(yùn)行回調(diào)(回調(diào)中需要刷新 TextView 重繪) | 整段圖文混排,支持 gif 和靜態(tài) emoji |
| makeEmojiSpannable() | SpannableString | classTag:PandaEmoView 依附的 ActivityTag(推薦使用 Activity.getLocalClassName());value : 待替換的文本 ;gif 運(yùn)行回調(diào)(回調(diào)中需要刷新 TextView 重繪) | 整段圖文混排,所以內(nèi)容轉(zhuǎn)換為靜態(tài) emoji |
| resumeGif() | void | activityTag : makeGifSpannable() 傳入的 tag 名 | 開始 activityTag 對應(yīng)的所有 gif 表情執(zhí)行 |
| pauseGif() | void | 暫停所有的 Gif 表情運(yùn)行 | |
| clearGif() | void | activityTag : makeGifSpannable() 傳入的 tag 名 | 停止 activityTag 對應(yīng)的所有 gif 表情執(zhí)行,并將期從任務(wù)棧中移除 |
關(guān)于內(nèi)存優(yōu)化
因?yàn)楸砬椋琯if 表情,自定義貼圖,表情包貼圖這些都涉及到圖片資源加載到內(nèi)存中。因此開發(fā)過程中不可避免的也遇到了許多的內(nèi)存優(yōu)化相關(guān)的問題。
工具
就地取材,直接使用 Android Studio 的 Monitors 工具可以直觀的查看到應(yīng)用運(yùn)行過程中內(nèi)存的變化過程
優(yōu)化點(diǎn)1 —— Gif 播放類的優(yōu)化
- 問題:
參考網(wǎng)上的 gif 圖文混排項(xiàng)目,雖然實(shí)現(xiàn)了 gif 與文字的圖文混排效果,但存在致命的缺陷。該項(xiàng)目中每一個(gè) gif 動(dòng)態(tài)表情圖都有一個(gè)對應(yīng)的 Runable 對象去執(zhí)行 gif 圖片的逐幀播放,當(dāng)一個(gè)表情重復(fù)輸入也會(huì)有新的 Runable 對象去執(zhí)行這樣的操作,這樣做的后果就是當(dāng)輸入的表情數(shù)量增加時(shí),所消耗的內(nèi)存是持續(xù)增長的。這顯然不能滿足生產(chǎn)使用的需求。 - 解決方案:
考慮到此處內(nèi)存增加的原因是讓表情動(dòng)起來的 Runable 泛濫引起的,因此減少 Runable 的數(shù)量就是解決此處內(nèi)存問題的關(guān)鍵。我的方案做的比較徹底,整個(gè)應(yīng)用 gif 表情這一塊兒都交給一個(gè) Runable 去處理,這個(gè) Runable 在 PandaTranslator 中進(jìn)行圖文轉(zhuǎn)化時(shí)會(huì)被初始化
// PandaTranslator 的 103 - 107 行
103 if (mGifRunnable == null) {
104 mGifRunnable = new GifRunnable(gifDrawable, mHandler);
105 } else {
106 mGifRunnable.addGifDrawable(gifDrawable);
107 }
因?yàn)?PandaTranslator 是一個(gè)單例實(shí)現(xiàn),所以在他初始化后 mGifRunnable 也將保持唯一性。無論是新建初始化還是 addGifDrawable() 都是把 Gif 表情對象放入 GifRunnable 中的一個(gè) Map 中。Map 的 key value 分別是表情控件依附的 Activity 的 LocalName 和 一個(gè) AnimatedGifDrawable 的 List。在 GifRunnable 的 run 方法中會(huì)根據(jù)當(dāng)前的 Activity 的 LocalName 去取出對應(yīng)的 AnimatedGifDrawable 列表,遍歷執(zhí)行并按第一張 gif 表情的幀間隔去刷新 Drawable 并觸發(fā) TextView 刷新回調(diào)
@Override
public void run() {
isRunning = true;
if (currentActivity != null) {
List<AnimatedGifDrawable> runningDrawables = mGifDrawableMap.get(currentActivity);
if (runningDrawables != null) {
for (AnimatedGifDrawable gifDrawable : runningDrawables) {
AnimatedGifDrawable.RunGifCallBack listener = gifDrawable.getUpdateListener();
List<AnimatedGifDrawable.RunGifCallBack> runningListener = listenersMap.get(currentActivity);
if (runningListener != null) {
// 避免一個(gè) TextView 多個(gè)表情時(shí)重復(fù)添加回調(diào)
if (!runningListener.contains(listener)) {
runningListener.add(listener);
}
} else {
// 為空時(shí)肯定不存在直接添加
runningListener = new ArrayList<>();
runningListener.add(listener);
listenersMap.put(currentActivity, runningListener);
}
gifDrawable.nextFrame();
}
for (AnimatedGifDrawable.RunGifCallBack callBack : listenersMap.get(currentActivity)) {
if (callBack != null) {
callBack.run();
}
}
frameDuration = runningDrawables.get(0).getFrameDuration();
}
}
mHandler.postDelayed(this, frameDuration);
}
這樣就實(shí)現(xiàn)了全局使用一個(gè) Runable 來執(zhí)行 gif 動(dòng)起來的任務(wù),不同的界面也僅需要將該界面的 AnimatedGifDrawable 對象加入任務(wù) Map 即可。
優(yōu)化點(diǎn)2 —— 界面暫?;蛲顺鰰r(shí) Gif 播放資源同步退出回收
上面說到的將 AnimatedGifDrawable 列表加入任務(wù) Map,只進(jìn)不出顯然是不科學(xué)的也會(huì)持續(xù)增加內(nèi)存的消耗。我們希望在 Activity 退出時(shí)能將將當(dāng)前 Activity 的 AnimatedGifDrawable 列表銷毀移除,在界面不可見但是可能會(huì)恢復(fù)時(shí)(pause 狀態(tài))暫停 Runable 的執(zhí)行,減少資源消耗。于是 GifRunable 提供了如下三個(gè)方法給外部調(diào)用
/**
* 使用了表情轉(zhuǎn)換的界面退出時(shí)調(diào)用,停止動(dòng)態(tài)圖handler
*/
public void clearHandler(String activityName) {
currentActivity = null;
//清除當(dāng)前頁的數(shù)據(jù)
mGifDrawableMap.remove(activityName);
// 當(dāng)退出當(dāng)前Activity后沒表情顯示時(shí)停止 Runable 清除所有動(dòng)態(tài)表情數(shù)據(jù)
listenersMap.remove(activityName);
if (mGifDrawableMap.size() == 0) {
clearAll();
}
}
private void clearAll() {
mHandler.removeCallbacks(this);
mHandler.removeCallbacksAndMessages(null);
mGifDrawableMap.clear();
isRunning = false;
}
/**
* 啟動(dòng)運(yùn)行
*/
public void startHandler(String activityName) {
currentActivity = activityName;
if (mGifDrawableMap != null && mGifDrawableMap.size() > 0 && !isRunning) {
run();
}
}
它的調(diào)用入口都在 PandaTranslator 中,然后我們只需在使用到 PandaEmoView 或者直接在 BaseActivity 的 onResume(),onPause(),onDestory() 中分別調(diào)用以下三個(gè)方法:
PandaTranslator.getInstance().resumeGif(activityLocalName);
PandaTranslator.getInstance().pauseGif();
PandaTranslator.getInstance().clearGif(activityLocalName)
優(yōu)化點(diǎn)3 —— 使用 LruCache 緩存 emoji 資源
根據(jù) LRU 規(guī)則將表情 Gif 緩存,避免重復(fù)加載創(chuàng)建新對象。
最后
因?yàn)殡x職從南京回到成都還有工作的各種各樣的原因,也是有四個(gè)多月沒更博客了。這是重新開始寫博客的第一篇,之后大概會(huì)以一個(gè)月 2-3 篇的樣子更新,記錄與分享,歡迎大家關(guān)注我的簡書。
本庫地址 PandaEmoView 歡迎 star 和提 issue