仿微信表情輸入鍵盤(支持 Gif 表情圖文混排 )

簡介

自定義的表情輸入鍵盤在很多應(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ù)器下載。
assets 目錄圖片
assets 目錄圖片
  • emoji 表情配置文件
config 截圖
config 截圖
  • 非自定義 sticker 配置文件(自定義 sticker 是沒有配置文件的)
sticker config 截圖
sticker config 截圖

具體使用規(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)容即可

布局規(guī)則說明
布局規(guī)則說明

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

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

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

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