Android一步一步教你實現(xiàn)Emoji表情鍵盤
說到聊天,就離不開文字、表情和圖片,表情和圖片增加了聊天的趣味性,讓原本無聊的文字瞬間用表情動了起來,今天給大家?guī)淼氖潜砬殒I盤,教你一步一步實現(xiàn),先來看下效果圖:

功能:
1、如何控制表情鍵盤與輸入法的切換
2、如何解析表情
3、如何處理表情與非表情的刪除
實現(xiàn):
明確了各個要解決的問題,下面我們逐個來實現(xiàn)
表情鍵盤與輸入法切換
查了一下相關(guān)資料,有如下方案:
方案一:動態(tài)改變SoftInputMode
軟鍵盤顯示時將SoftInputMode設(shè)置為「stateVisible|adjustResize」,表情鍵盤顯示時調(diào)整為「adjustPan」
方案二:Dialog
直接在軟鍵盤上顯示一個Dialog,可避開大部分切換邏輯,但是在打開當前頁面后存在軟鍵盤和Dialog沖突問題
觀察QQ、微信、微博、陌陌后發(fā)現(xiàn),他們的表情鍵盤和軟鍵盤切換,并不會導(dǎo)致聊天內(nèi)容(ListView、RecyclerView)的跳動,基本就可以推測SoftInputMode就是adjustsPan。
明確了adjustPan那就好辦了,既然聊天內(nèi)容(ListView、RecyclerView)不會跳動,那么在軟鍵盤切換至表情鍵盤的時候,底部肯定有一個和軟鍵盤高度一致的View,只需在點擊表情的時候?qū)④涙I盤隱藏,顯示表情鍵盤,在點擊EditText的時候顯示軟鍵盤,隱藏表情鍵盤。
來梳理一下知識點:
1、如何獲取軟鍵盤高度
2、如何手動控制軟鍵盤的顯示與隱藏
3、如何避免在別的頁面切到當前界面因軟鍵盤的狀態(tài)變化而沖突
獲取軟鍵盤高度
private int getSupportSoftInputHeight() {Rect r = new Rect();mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r);int screenHeight = mActivity.getWindow().getDecorView().getRootView().getHeight();int softInputHeight = screenHeight - r.bottom;if (Build.VERSION.SDK_INT >= 20) {// When SDK Level >= 20 (Android L),// the softInputHeight will contain the height of softButtonsBar (if has)softInputHeight = softInputHeight - getSoftButtonsBarHeight();}if (softInputHeight < 0) {Log.w("EmotionInputDetector", "Warning: value of softInputHeight is below zero!");}if (softInputHeight > 0) {sp.edit().putInt(SHARE_PREFERENCE_TAG, softInputHeight).apply();}return softInputHeight;}
這里的原理是通過當前Activity獲取RootView的高度減去Activity自身的高度,就得到了軟鍵盤的高度,但是發(fā)現(xiàn)在有虛擬按鍵的手機上在沒有顯示軟鍵盤時減出來的高度總是144,后來查了下資料,發(fā)現(xiàn)在API>18時有軟鍵盤的手機需要減去底部虛擬按鍵的高度。
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)private int getSoftButtonsBarHeight() {DisplayMetrics metrics = new DisplayMetrics();mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);int usableHeight = metrics.heightPixels;mActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);int realHeight = metrics.heightPixels;if (realHeight > usableHeight) {return realHeight - usableHeight;} else {return 0;}}
把獲取到的高度設(shè)置給表情鍵盤
private void showEmotionLayout() {int softInputHeight = getSupportSoftInputHeight();if (softInputHeight == 0) {softInputHeight = sp.getInt(SHARE_PREFERENCE_TAG, 400);}hideSoftInput();mEmotionLayout.getLayoutParams().height = softInputHeight;mEmotionLayout.setVisibility(View.VISIBLE);}
控制表情的顯示與隱藏
private void showSoftInput() {mEditText.requestFocus();mEditText.post(new Runnable() {@Overridepublic void run() {mInputManager.showSoftInput(mEditText, 0);}});}private void hideSoftInput() {mInputManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);}
在測試后發(fā)現(xiàn)一個問題,點擊表情按鈕,輸入框會抖動,分析下這個過程,點擊表情按鈕,關(guān)閉軟鍵盤,此時Activity的高度發(fā)生變化,高度變高,輸入框回到底部,再打開表情鍵盤,此時輸入框又被頂上來,輸入框看起來上下抖動,經(jīng)多次測試發(fā)現(xiàn)無論是先隱藏軟鍵盤還是先顯示表情鍵盤都存在這個問題,思考過后,既然輸入框會上下抖動,那么固定它的位置不就行了,那么問題來了,如何固定它的位置呢?
舉個栗子,假如在一個LinearLayout里面有若干個控件,如果里面的控件的位置大小都不變,那么即使在軟鍵盤顯示和隱藏(Activity的高度發(fā)生變化),也不會隱藏輸入框的位置,自然也就不會發(fā)生跳動問題。
鎖定解鎖內(nèi)容高度(ListView、RecyclerView)
private void lockContentHeight() {LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();params.height = mContentView.getHeight();params.weight = 0.0F;}private void unlockContentHeightDelayed() {mEditText.postDelayed(new Runnable() {@Overridepublic void run() {((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;}}, 200L);}
表情面板控制
public EmotionInputDetector bindToEmotionButton(final CheckBox emotionButton) {mEmojiView = emotionButton;emotionButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (mEmotionLayout.isShown()) {lockContentHeight();hideEmotionLayout(true);mEmojiView.setChecked(false);unlockContentHeightDelayed();} else {if (isSoftInputShown()) {lockContentHeight();showEmotionLayout();mEmojiView.setChecked(true);unlockContentHeightDelayed();} else {showEmotionLayout();}}}});return this;}
表情解析
問題分析:
1、如何將表情碼和表情建立聯(lián)系
2、如何給表情分頁
3、如何將表情碼轉(zhuǎn)換成表情
將表情碼和表情以鍵值對的形式建立聯(lián)系
ArrayMap?emoJiMap?=?new?ArrayMap(); key(表情碼)value(表情地址)
emoJiMap.put("[emoji_1]",R.drawable.emoji_1);emoJiMap.put("[emoji_2]",R.drawable.emoji_2);emoJiMap.put("[emoji_3]",R.drawable.emoji_3);emoJiMap.put("[emoji_4]",R.drawable.emoji_4);emoJiMap.put("[emoji_5]",R.drawable.emoji_5);emoJiMap.put("[emoji_6]",R.drawable.emoji_6);emoJiMap.put("[emoji_7]",R.drawable.emoji_7);emoJiMap.put("[emoji_8]",R.drawable.emoji_8);emoJiMap.put("[emoji_9]",R.drawable.emoji_9);emoJiMap.put("[emoji_10]",R.drawable.emoji_10);emoJiMap.put("[emoji_11]",R.drawable.emoji_11);emoJiMap.put("[emoji_12]",R.drawable.emoji_12);emoJiMap.put("[emoji_13]",R.drawable.emoji_13);emoJiMap.put("[emoji_14]",R.drawable.emoji_14);emoJiMap.put("[emoji_15]",R.drawable.emoji_15);emoJiMap.put("[emoji_16]",R.drawable.emoji_16);emoJiMap.put("[emoji_17]",R.drawable.emoji_17);emoJiMap.put("[emoji_18]",R.drawable.emoji_18);emoJiMap.put("[emoji_19]",R.drawable.emoji_19);emoJiMap.put("[emoji_20]",R.drawable.emoji_20);
將表情面板的表情碼用List進行保存
List emojiList = new ArrayList(); emojiList.add("[emoji_1]");emojiList.add("[emoji_2]");emojiList.add("[emoji_3]");emojiList.add("[emoji_4]");emojiList.add("[emoji_5]");emojiList.add("[emoji_6]");emojiList.add("[emoji_7]");emojiList.add("[emoji_8]");emojiList.add("[emoji_9]");emojiList.add("[emoji_10]");emojiList.add("[emoji_11]");emojiList.add("[emoji_12]");emojiList.add("[emoji_13]");emojiList.add("[emoji_14]");emojiList.add("[emoji_15]");emojiList.add("[emoji_16]");emojiList.add("[emoji_17]");emojiList.add("[emoji_18]");emojiList.add("[emoji_19]");emojiList.add("[emoji_20]");
計算表情頁
public ListgetPagers() { ListpageViewList = new ArrayList<>(); //每一頁表情的viewmPageNum = (int) Math.ceil(mEmoJiResList.size() * 1.0f / EMOJI_PAGE_COUNT);for (int position = 1; position <= mPageNum; position++) {pageViewList.add(getGridView(position));}return pageViewList;}
表情分頁
public View getGridView(int position) {List mEmoJiList = new ArrayList<>();View containerView = View.inflate(mContext, R.layout.container_gridview, null);ExpandGridView eg_gridView = (ExpandGridView) containerView.findViewById(R.id.eg_gridView);eg_gridView.setGravity(Gravity.CENTER_VERTICAL);ListemojiPageList = null; if (position == mPageNum)//最后一頁emojiPageList = mEmoJiResList.subList((position - 1) * EMOJI_PAGE_COUNT, mEmoJiResList.size());elseemojiPageList = mEmoJiResList.subList((position - 1) * EMOJI_PAGE_COUNT, EMOJI_PAGE_COUNT * position);mEmoJiList.addAll(emojiPageList);//添加刪除表情mEmoJiList.add("[刪除]");final EmoJiAdapter mEmoJiAdapter = new EmoJiAdapter(mContext, position, mEmoJiList);eg_gridView.setAdapter(mEmoJiAdapter);eg_gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView> parent, View view, int positionIndex, long id) {String fileName = mEmoJiAdapter.getItem(positionIndex);if (fileName != "[刪除]") { // 不是刪除鍵,顯示表情showEmoJi(fileName);} else { // 刪除文字或者表情deleteContent();}}});return containerView;}
將表情面板的表情碼轉(zhuǎn)解析成表情
@Overridepublic View getView(int position, View convertView, ViewGroup parent) {if (convertView == null) {convertView = View.inflate(getContext(), R.layout.item_row_emoji, null);}ImageView imageView = (ImageView) convertView.findViewById(R.id.iv_emoji);String fileName = getItem(position);Integer resId = EmoJiUtils.getEmoJiMap().get(fileName);if (resId != null) {Drawable drawable = getContext().getResources().getDrawable(resId);drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());imageView.setImageResource(resId);}return convertView;}
輸入框表情碼轉(zhuǎn)換成表情
public static SpannableString parseEmoJi(Context context, String content) {SpannableString spannable = new SpannableString(content);String reg = "\\[[a-zA-Z0-9_\\u4e00-\\u9fa5]+\\]";//校驗表情正則Pattern pattern = Pattern.compile(reg);Matcher matcher = pattern.matcher(content);while (matcher.find()) {String regEmoJi = matcher.group();//獲取匹配到的emoji字符串int start = matcher.start();//匹配到字符串的開始位置int end = matcher.end();//匹配到字符串的結(jié)束位置Integer resId = emoJiMap.get(regEmoJi);//通過emoji名獲取對應(yīng)的表情idif (resId != null) {Drawable drawable = context.getResources().getDrawable(resId);drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());ImageSpan imageSpan = new ImageSpan(drawable, content);spannable.setSpan(imageSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);}}return spannable;}
private void showEmoJi(String fileName) {int selectionStart = mInputContainer.getSelectionStart();String body = mInputContainer.getText().toString();StringBuilder stringBuilder = new StringBuilder(body);stringBuilder.insert(selectionStart, fileName);mInputContainer.setText(EmoJiUtils.parseEmoJi(mContext, stringBuilder.toString()));mInputContainer.setSelection(selectionStart + fileName.length());????}
表情刪除
private void deleteContent() {if (!TextUtils.isEmpty(mInputContainer.getText())) {int selectionStart = mInputContainer.getSelectionStart();//獲取光標位置if (selectionStart > 0) {String body = mInputContainer.getText().toString();String lastStr = body.substring(selectionStart - 1, selectionStart);//獲取最后一個字符if (lastStr.equals("]")) {//表情if (selectionStart < body.length()) {//從中間開始刪除body = body.substring(0, selectionStart);}int i = body.lastIndexOf("[");if (i != -1) {String tempStr = body.substring(i, selectionStart);//截取表情碼if (EmoJiUtils.getEmoJiMap().containsKey(tempStr)) {//校驗是否是表情mInputContainer.getEditableText().delete(i, selectionStart);//刪除表情} else {mInputContainer.getEditableText().delete(selectionStart - 1, selectionStart);//刪除一個字符}} else {mInputContainer.getEditableText().delete(selectionStart - 1, selectionStart);}} else {//非表情mInputContainer.getEditableText().delete(selectionStart - 1, selectionStart);}}}}
效果圖:

源碼地址:
https://github.com/diycoder/EasyEmoji
到這里就結(jié)束啦。
