簡介
自定義的表情輸入鍵盤在很多應用中都會有用到,譬如微信、QQ 等社交聊天軟件中更是不可缺少的部分。本文將解析一下個人的自定義表情輸入控件庫 PandaEmoView 的實現和使用。
該庫具有以下特點:
- 支持 emoji 表情圖片
- 支持 gif 動態表情輸入顯示
- 支持單張貼圖表情(與微信收藏表情一致)
- 支持題圖表情庫的添加刪除
效果圖:
快速使用
引入庫
compile 'com.pandaq:PandaEmoView:1.0.0'
表情資源及配置文件
- 默認的 emoji 和 gif 表情以及他們的配置文件是放在開發包 assets 目錄下的,若表情比較多比較大也可自行修改源碼在 APP 啓動時從服務器下載。
- emoji 表情配置文件
- 非自定義 sticker 配置文件(自定義 sticker 是沒有配置文件的)
具體使用規則
與表情輸入控件相關的 EditText 必須使用 PandaEditText
PandaEditText 只是重寫了 onKeyPreIme() 獲取按返回鍵的通知,繼承自 EditText 的控件可繼承 PandaEditText 自定義
1.應用 Application 中進行全局參數配置
private void configPandaEmoView() {
new PandaEmoManager.Builder()
.with(getApplicationContext()) // 傳遞 Context
.configFileName("emoji.xml")// 配置文件名稱
.emoticonDir("face") // asset 下存放表情的目錄路徑(asset——> configFileName 之間的路徑,結尾不帶斜槓)
.sourceDir("images") // 存放 emoji 表情資源文件夾路徑(emoticonDir 圖片資源之間的路徑,結尾不帶斜槓)
.showAddTab(true)//tab欄是否顯示添加按鈕
.showStickers(true)//tab欄是否顯示貼圖切換按鍵
.showSetTab(true)//tab欄是否顯示設置按鈕
.defaultBounds(30)//emoji 表情顯示出來的寬高
.cacheSize(1024)//加載資源到內存時 LruCache 緩存大小
.defaultTabIcon(R.drawable.ic_default)//emoji表情Tab欄圖標
.emojiColumn(7)//單頁顯示錶情的列數
.emojiRow(3)//單頁顯示錶情的行數
.stickerRow(2)//單頁顯示貼圖表情的行數
.stickerColumn(4)//單頁顯示貼圖表情的列數
.maxCustomStickers(30)//允許添加的收藏表情數
.imageLoader(new IImageLoader() {
@Override
public void displayImage(String path, ImageView imageView) { // 加載貼圖表情的圖片加載接口
Picasso.with(getApplicationContext())
.load(path)
.fit()
.centerCrop()
.into(imageView);
}
})
.build(); //構建 PandaEmoManager 單利
}
2.使用此控件的 Activity 在 manifest 文件中配置
// 這句是一定要加上的。
android:windowSoftInputMode="adjustResize"
3.使用此控件的界面 xml 文件規則
佈局規則如下圖,lockView 即是我們正常顯示內容的 View 它與表情輸入控件 PandaEmoView 屬於同一層級,父佈局必須爲縱向線性佈局,且設置 lockView 權重爲 1 ,PandaEmoView 高度包裹內容即可
4.使用控件的 Activity Java 代碼設置
//界面控件初始化後 .attachEditText()綁定輸入控件
//初始化 KeyBoardManager,PandaEmoView.attachEditText() 必須在後調用
主要使用類及公有方法概覽
PandaEmoEditText
- 表情輸入框繼承自
EditText
只對onKeyIme()
進行復寫用於監聽輸入鍵盤或者軟鍵盤的彈出與關閉
PandaEmoView
- 表情輸入控件 View 繼承自
RelativeLayout
方法 | 返回值 | 參數 | 描述 |
---|---|---|---|
attachEditText(PandaEmoEditText input) | void | 表情輸入控件(如使用自定義 EditText 可直接繼承重寫) | 綁定輸入控件 |
getAttachEditText() | void | 獲取當前表情控件綁定的輸入框控件 | |
reloadEmos | void | position 重載表情控件數據後默認選中 Tab 的 position | 添加或者刪除表情數據後重載刷新表情控件 |
PandaEmoManager
-
PandaEmoManager
爲核心配置類,表情控件的各種參數都通過此類的構造器進行配置
屬性 | 類型 | 描述 | 默認值 |
---|---|---|---|
EMOT_DIR | String | 默認表情圖在 assets 中的路徑 (assets 目錄到配置 xml 文件之間的部分的路徑,demo 中的 face) | face |
SOURCE_DIR | String | EMOT_DIR 目錄下存放圖片資源的文件夾路徑 (demo 中的 images) | source_default |
STICKER_PATH | String | Sticker貼圖包的存放目錄,該目錄下的每一個文件目錄都爲一個貼圖包 | /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 | 最大添加的自定義貼圖表情數 | 30 |
EMOJI_ROW | int | emoji 表情單頁行數 | 3 |
EMOJI_COLUMN | int | emoji 表情單頁列數 | 7 |
STICKER_ROW | int | sticker 表情單頁行數 | 2 |
STICKER_COLUMN | int | sticker 表情單頁列數 | 4 |
showAddButton | boolean | tab 欄是否顯示添加按鈕 | ture |
showSetButton | boolean | tab 欄是否顯示設置按鈕 | ture |
showStickers | boolean | tab 欄是否顯示貼圖按鈕(所以 sticker) | ture |
方法 | 返回值 | 參數 | 描述 |
---|---|---|---|
init() | void | 無參 | 初始化 拼接 Sticker 路徑及創建自定義貼圖 文件夾 (STICKER_PATH + "/selfSticker" ) |
makePattern() | Pattern | 無參 | 創建 emoji 正則匹配器 |
剩餘方法都爲屬性值的 getter() setter() 不在贅述。
PandaEmoManager.Builder
-
PandaEmoManager
的構造器類,屬性及方法都與 PandaEmoManager 一一對應;
KeyBoardManager
-
KeyBoardManager
爲輸入法軟鍵盤與表情輸入控件協調管理類
屬性 | 類型 | 描述 | 默認值 |
---|---|---|---|
SHARE_PREFERENCE_NAME | String | 用於存儲鍵盤高度的 SP 的名字 | "EmotionKeyBoard" |
SHARE_PREFERENCE_SOFT_INPUT_HEIGHT | String | 用於存儲鍵盤高度的 key | "EmotionKeyBoard" |
mActivity | Activity | 控件依附的 Activity 界面 | null |
mEmotionView | PandaEmoView | 當前管理的表情輸入控件 | null |
interceptBackPress | boolean | 是否攔截返回鍵 | false |
lockView | View | 鎖定高度的 View(即同一線性父佈局中,表情控件之外的佈局視圖) | null |
mOnEmotionButtonOnClickListener | OnEmotionButtonOnClickListener | 表情顯示控制按鈕監聽 | null |
mOnInputShowListener | OnInputShowListener | 監聽輸入欄的彈出與關閉 | null |
方法 | 返回值 | 參數 | 描述 |
---|---|---|---|
with() | KeyBoardManager | Activity : 當前輸入控件依附的 Activity | 初始化 KeyBoardManager 創建單例 |
bindToLockContent() | KeyBoardManager | lockView:切換需要鎖定高度的 View | 賦值給內部屬性 lockView |
bindToEmotionButton() | KeyBoardManager | View...: 多個 View 參數,切換控制按鈕 | 爲輸入控件綁定控制按鈕 |
setEmotionView() | KeyBoardManager | PandaEmoView: 被管理的表情控件 | 綁定當前管理的輸入控件(綁定的控件必須在此之前調用 PandaEmoView.attachEditText() 否則內部將會空指針) |
interceptBackPress() | boolean | null | 在 Activity 的 backPressd() 中檢查是否需要攔截返回鍵關閉輸入欄而不是退出界面 |
hideInputLayout() | void | null | 供外部調用關閉輸入欄(不能未初始化直接調用) |
showInputLayout() | void | null | 供外部調用顯示輸入欄(不能未初始化直接調用) |
EmoticonManager
-
EmoticonManager
爲 emoji 表情加載管理類,此類提供方法將資源文件根據配置文件加載進內存,方法大多數爲私有方法,源碼中可查看註釋。
StickerManager
-
StickerManager
爲 sticker 表情加載管理類,此類提供方法將資源文件根據配置文件加載進內存,與EmoticonManager
類似
PandaEmoTranslator
-
PandaEmoTranslator
爲 emoji 表情 [文字] 轉表情的轉換工具類
方法 | 返回值 | 參數 | 描述 |
---|---|---|---|
getInstance() | PandaEmoTranslator | null | 獲取文本表情轉換器單例 |
setMaxGifPerView() | void | maxGifPerView: 每個 TextView 控件最多顯示動態表情的個數 | 設置每個 TextView 控件最多顯示動態表情的個數,超過此數全部顯示爲靜態表情 |
getMaxGifPerView() | int | null | 獲取每個 TextView 控件最多顯示動態表情的個數 |
makeGifSpannable() | SpannableString | classTag:PandaEmoView 依附的 ActivityTag(推薦使用 Activity.getLocalClassName());value : 待替換的文本 ;gif 運行回調(回調中需要刷新 TextView 重繪) | 整段圖文混排,支持 gif 和靜態 emoji |
makeEmojiSpannable() | SpannableString | classTag:PandaEmoView 依附的 ActivityTag(推薦使用 Activity.getLocalClassName());value : 待替換的文本 ;gif 運行回調(回調中需要刷新 TextView 重繪) | 整段圖文混排,所以內容轉換爲靜態 emoji |
resumeGif() | void | activityTag : makeGifSpannable() 傳入的 tag 名 | 開始 activityTag 對應的所有 gif 表情執行 |
pauseGif() | void | 暫停所有的 Gif 表情運行 | |
clearGif() | void | activityTag : makeGifSpannable() 傳入的 tag 名 | 停止 activityTag 對應的所有 gif 表情執行,並將期從任務棧中移除 |
關於內存優化
因爲表情,gif 表情,自定義貼圖,表情包貼圖這些都涉及到圖片資源加載到內存中。因此開發過程中不可避免的也遇到了許多的內存優化相關的問題。
工具
就地取材,直接使用 Android Studio 的 Monitors 工具可以直觀的查看到應用運行過程中內存的變化過程
優化點1 —— Gif 播放類的優化
- 問題:
參考網上的 gif 圖文混排項目,雖然實現了 gif 與文字的圖文混排效果,但存在致命的缺陷。該項目中每一個 gif 動態表情圖都有一個對應的 Runable 對象去執行 gif 圖片的逐幀播放,當一個表情重複輸入也會有新的 Runable 對象去執行這樣的操作,這樣做的後果就是當輸入的表情數量增加時,所消耗的內存是持續增長的。這顯然不能滿足生產使用的需求。 - 解決方案:
考慮到此處內存增加的原因是讓表情動起來的 Runable 氾濫引起的,因此減少 Runable 的數量就是解決此處內存問題的關鍵。我的方案做的比較徹底,整個應用 gif 表情這一塊兒都交給一個 Runable 去處理,這個 Runable 在 PandaTranslator 中進行圖文轉化時會被初始化
// PandaTranslator 的 103 - 107 行
103 if (mGifRunnable == null) {
104 mGifRunnable = new GifRunnable(gifDrawable, mHandler);
105 } else {
106 mGifRunnable.addGifDrawable(gifDrawable);
107 }
因爲 PandaTranslator 是一個單例實現,所以在他初始化後 mGifRunnable 也將保持唯一性。無論是新建初始化還是 addGifDrawable() 都是把 Gif 表情對象放入 GifRunnable 中的一個 Map 中。Map 的 key value 分別是表情控件依附的 Activity 的 LocalName 和 一個 AnimatedGifDrawable 的 List。在 GifRunnable 的 run 方法中會根據當前的 Activity 的 LocalName 去取出對應的 AnimatedGifDrawable 列表,遍歷執行並按第一張 gif 表情的幀間隔去刷新 Drawable 並觸發 TextView 刷新回調
@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) {
// 避免一個 TextView 多個表情時重複添加回調
if (!runningListener.contains(listener)) {
runningListener.add(listener);
}
} else {
// 爲空時肯定不存在直接添加
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);
}
這樣就實現了全局使用一個 Runable 來執行 gif 動起來的任務,不同的界面也僅需要將該界面的 AnimatedGifDrawable 對象加入任務 Map 即可。
優化點2 —— 界面暫停或退出時 Gif 播放資源同步退出回收
上面說到的將 AnimatedGifDrawable 列表加入任務 Map,只進不出顯然是不科學的也會持續增加內存的消耗。我們希望在 Activity 退出時能將將當前 Activity 的 AnimatedGifDrawable 列表銷燬移除,在界面不可見但是可能會恢復時(pause 狀態)暫停 Runable 的執行,減少資源消耗。於是 GifRunable 提供瞭如下三個方法給外部調用
/**
* 使用了表情轉換的界面退出時調用,停止動態圖handler
*/
public void clearHandler(String activityName) {
currentActivity = null;
//清除當前頁的數據
mGifDrawableMap.remove(activityName);
// 當退出當前Activity後沒表情顯示時停止 Runable 清除所有動態表情數據
listenersMap.remove(activityName);
if (mGifDrawableMap.size() == 0) {
clearAll();
}
}
private void clearAll() {
mHandler.removeCallbacks(this);
mHandler.removeCallbacksAndMessages(null);
mGifDrawableMap.clear();
isRunning = false;
}
/**
* 啓動運行
*/
public void startHandler(String activityName) {
currentActivity = activityName;
if (mGifDrawableMap != null && mGifDrawableMap.size() > 0 && !isRunning) {
run();
}
}
它的調用入口都在 PandaTranslator 中,然後我們只需在使用到 PandaEmoView 或者直接在 BaseActivity 的 onResume(),onPause(),onDestory() 中分別調用以下三個方法:
PandaTranslator.getInstance().resumeGif(activityLocalName);
PandaTranslator.getInstance().pauseGif();
PandaTranslator.getInstance().clearGif(activityLocalName)
優化點3 —— 使用 LruCache 緩存 emoji 資源
根據 LRU 規則將表情 Gif 緩存,避免重複加載創建新對象。
最後
因爲離職從南京回到成都還有工作的各種各樣的原因,也是有四個多月沒更博客了。這是重新開始寫博客的第一篇,之後大概會以一個月 2-3 篇的樣子更新,記錄與分享,歡迎大家關注我的簡書。
本庫地址 PandaEmoView 歡迎 star 和提 issue