安卓開發-最簡單快速的仿微信聊天實現-附贈微信原生表情,QQ原生表情
前言;正常實現聊天功能想必大家都使用三方的Sdk比如環信融雲集成的,但是聊天記錄的儲存只能有三天,想增加儲存時長就需要花錢,so 我只好自己想辦法實現了,這個demo是類似於留言板,並非即時通訊!只實現了表情文字圖文混排,可以通過手動重新整理實現即時通訊ok廢話少說,先看效果圖;
專案下載地址
https://github.com/PangHaHa12138/TestChatdemo
表情下載地址
大體思路:
1.先從佈局開始,聊天介面就是多條目的listview,左右各算一種型別,這樣正常語音,文字,圖文混排,大圖片,大表情,視訊,都是x2倍的,聊天條目就是listview條目背景透明,然後imageview+給textview設定氣泡的聊天背景
不過我現在暫時只實現了文字表情,然後鍵盤這塊是gridview實現,寫正則來過濾表情的編碼,點選事件判斷選中表情還是刪除
感謝開源鍵盤控制元件:
https://github.com/w446108264/XhsEmoticonsKeyboard
繼承XhsEmoticonsKeyboard控制元件寫一個帶表情的鍵盤
2.程式碼大體邏輯,傳送的訊息其實就是一個文字內容,通過SpannableStringBuilder和Spannable實現圖文混排,其實就是把各種表情序列號也可以說是索引寫到一個xml檔案裡,然後用一個map來存這些圖片對應的編碼,其實就是圖片名字,然後通過正則來找到正確的索引,即xml裡存的圖片對應資料夾drawable下的
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(content); Spannable spannable = QqFilter.spannableFilter(tv_content.getContext(), spannableStringBuilder, content, EmoticonsKeyboardUtils.getFontHeight(tv_content), null); tv_content.setText(spannable);
然後就是listview的展示了,頁面初始化的時候根據伺服器返回的欄位判斷是別人發的在左邊,還是我發的在右面,然後在adapter裡呼叫圖文混排的方法找到對應表情圖片填充條目,傳送的時候先檢查是否是正確的表情字元,找到對應表情圖片的集合map,找到對應的表情名字,然後和文字一起傳到伺服器,然後進行網路請求,上傳成功之後再重新整理介面,上拉載入的話是載入歷史記錄全部的,下拉重新整理是請求最新的,每次請求都控制頁面顯示最多28條資料,也就是4頁,上來初始化頁面也是,然後可以通過聊完上拉不停的重複手動實現即時通訊
當然這是開玩笑了
下面上程式碼:
主要佈局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:id="@+id/titlefragment" android:name="com.panghaha.it.testchatdemo.Titlefragment" android:layout_width="match_parent" android:layout_height="45dp"/> <!--<TextView--> <!--android:layout_centerInParent="true"--> <!--android:textSize="22sp"--> <!--android:text="在未來的日子裡,\n努力讓拋棄我的人\n始終覺得她們的決定是正確的"--> <!--android:layout_width="wrap_content"--> <!--android:layout_height="wrap_content" />--> <FrameLayout android:id="@+id/content" android:layout_above="@+id/bottom_navigation_bar" android:layout_below="@+id/titlefragment" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> <com.ashokvarma.bottomnavigation.BottomNavigationBar android:id="@+id/bottom_navigation_bar" android:layout_gravity="bottom" android:layout_alignParentBottom="true" android:layout_width="match_parent" android:layout_height="wrap_content"/> </RelativeLayout>主介面,下面四個按鈕,切換四個fragment
聊天介面佈局 最外層用自己繼承
XhsEmoticonsKeyBoard 鍵盤類
<com.panghaha.it.testchatdemo.common.SimpleUserdefEmoticonsKeyBoard xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:id="@+id/keyboard" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="vertical" android:background="@drawable/pic_bg3x" android:layout_width="match_parent" android:layout_height="match_parent"> <!--<include layout="@layout/activity_right_toobar"/>--> <android.support.v7.widget.Toolbar android:id="@+id/toobaraaa" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/titbar" android:minHeight="?attr/actionBarSize"> <TextView android:id="@+id/toolbarmtit" style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:lines="1" android:ellipsize="end" android:text="安琪寶貝" android:scrollHorizontally="true" android:textColor="@color/white" android:layout_gravity="center" /> <!--自定義toolbar的title 和subtitle --> </android.support.v7.widget.Toolbar> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/uploadmore" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/lv_chat" android:layout_width="match_parent" android:layout_height="match_parent" android:cacheColorHint="#00000000" android:divider="@null" android:fadingEdge="none" android:fitsSystemWindows="true" android:listSelector="#00000000" android:scrollbarStyle="outsideOverlay" android:scrollingCache="false" android:smoothScrollbar="true" android:stackFromBottom="true" /> </android.support.v4.widget.SwipeRefreshLayout> </LinearLayout> </com.panghaha.it.testchatdemo.common.SimpleUserdefEmoticonsKeyBoard>
鍵盤類
public class SimpleUserdefEmoticonsKeyBoard extends XhsEmoticonsKeyBoard { public final int APPS_HEIGHT = 120; public SimpleUserdefEmoticonsKeyBoard(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void inflateKeyboardBar(){ mInflater.inflate(R.layout.view_keyboard_userdef, this); } @Override protected View inflateFunc(){ return mInflater.inflate(R.layout.view_func_emoticon_userdef, null); } @Override public void reset() { EmoticonsKeyboardUtils.closeSoftKeyboard(getContext()); mLyKvml.hideAllFuncView(); mBtnFace.setImageResource(R.drawable.chatting_emoticons); } @Override public void onFuncChange(int key) { if (FUNC_TYPE_EMOTION == key) { mBtnFace.setImageResource(R.drawable.chatting_softkeyboard); } else { mBtnFace.setImageResource(R.drawable.chatting_emoticons); } checkVoice(); } @Override public void OnSoftClose() { super.OnSoftClose(); if (mLyKvml.getCurrentFuncKey() == FUNC_TYPE_APPPS) { setFuncViewHeight(EmoticonsKeyboardUtils.dip2px(getContext(), APPS_HEIGHT)); } } @Override protected void showText() { mEtChat.setVisibility(VISIBLE); mBtnFace.setVisibility(VISIBLE); mBtnVoice.setVisibility(GONE); } @Override protected void showVoice() { mEtChat.setVisibility(GONE); mBtnFace.setVisibility(GONE); mBtnVoice.setVisibility(VISIBLE); reset(); } @Override protected void checkVoice() { if (mBtnVoice.isShown()) { mBtnVoiceOrText.setImageResource(R.drawable.chatting_softkeyboard); } else { mBtnVoiceOrText.setImageResource(R.drawable.chatting_vodie); } } @Override public void onClick(View v) { int i = v.getId(); if (i == com.keyboard.view.R.id.btn_voice_or_text) { if (mEtChat.isShown()) { mBtnVoiceOrText.setImageResource(R.drawable.chatting_softkeyboard); showVoice(); } else { showText(); mBtnVoiceOrText.setImageResource(R.drawable.chatting_vodie); EmoticonsKeyboardUtils.openSoftKeyboard(mEtChat); } } else if (i == com.keyboard.view.R.id.btn_face) { toggleFuncView(FUNC_TYPE_EMOTION); } else if (i == com.keyboard.view.R.id.btn_multimedia) { toggleFuncView(FUNC_TYPE_APPPS); setFuncViewHeight(EmoticonsKeyboardUtils.dip2px(getContext(), APPS_HEIGHT)); } }表情過濾和定位類
public class QqFilter extends EmoticonFilter { public static final int WRAP_DRAWABLE = -1; private int emoticonSize = -1; public static final Pattern QQ_RANGE = Pattern.compile("\\[[a-zA-Z0-9\\u4e00-\\u9fa5]+\\]"); public static Matcher getMatcher(CharSequence matchStr) { return QQ_RANGE.matcher(matchStr); } @Override public void filter(EditText editText, CharSequence text, int start, int lengthBefore, int lengthAfter) { emoticonSize = emoticonSize == -1 ? EmoticonsKeyboardUtils.getFontHeight(editText) : emoticonSize; clearSpan(editText.getText(), start, text.toString().length()); Matcher m = getMatcher(text.toString().substring(start, text.toString().length())); if (m != null) { while (m.find()) { String key = m.group(); int icon = DefQqEmoticons.sQqEmoticonHashMap.get(key); if (icon > 0) { emoticonDisplay(editText.getContext(), editText.getText(), icon, emoticonSize, start + m.start(), start + m.end()); } } } } public static Spannable spannableFilter(Context context, Spannable spannable, CharSequence text, int fontSize, EmojiDisplayListener emojiDisplayListener) { Matcher m = getMatcher(text); if (m != null) { while (m.find()) { String key = m.group(); int icon = DefQqEmoticons.sQqEmoticonHashMap.get(key); if (emojiDisplayListener == null) { if (icon > 0) { emoticonDisplay(context, spannable, icon, fontSize, m.start(), m.end()); } } else { emojiDisplayListener.onEmojiDisplay(context, spannable, "" + icon, fontSize, m.start(), m.end()); } } } return spannable; } private void clearSpan(Spannable spannable, int start, int end) { if (start == end) { return; } EmoticonSpan[] oldSpans = spannable.getSpans(start, end, EmoticonSpan.class); for (int i = 0; i < oldSpans.length; i++) { spannable.removeSpan(oldSpans[i]); } } public static void emoticonDisplay(Context context, Spannable spannable, int emoticon, int fontSize, int start, int end) { Drawable drawable = getDrawable(context, emoticon); if (drawable != null) { int itemHeight; int itemWidth; if (fontSize == WRAP_DRAWABLE) { itemHeight = drawable.getIntrinsicHeight(); itemWidth = drawable.getIntrinsicWidth(); } else { itemHeight = fontSize; itemWidth = fontSize; } drawable.setBounds(0, 0, itemHeight, itemWidth); EmoticonSpan imageSpan = new EmoticonSpan(drawable); spannable.setSpan(imageSpan, start, end, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } }生成新增各型別表情的工廠
package com.panghaha.it.testchatdemo.common; import android.content.Context; import android.text.Editable; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.TextView; import com.panghaha.it.testchatdemo.R; import com.sj.emoji.DefEmoticons; import com.sj.emoji.EmojiBean; import com.testemticon.DefXhsEmoticons; import java.io.IOException; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Collections; import sj.keyboard.adpater.EmoticonsAdapter; import sj.keyboard.adpater.PageSetAdapter; import sj.keyboard.data.EmoticonEntity; import sj.keyboard.data.EmoticonPageEntity; import sj.keyboard.data.EmoticonPageSetEntity; import sj.keyboard.data.PageEntity; import sj.keyboard.data.PageSetEntity; import sj.keyboard.interfaces.EmoticonClickListener; import sj.keyboard.interfaces.EmoticonDisplayListener; import sj.keyboard.interfaces.PageViewInstantiateListener; import sj.keyboard.utils.EmoticonsKeyboardUtils; import sj.keyboard.utils.imageloader.ImageBase; import sj.keyboard.utils.imageloader.ImageLoader; import sj.keyboard.widget.EmoticonPageView; import sj.keyboard.widget.EmoticonsEditText; import sj.qqkeyboard.DefQqEmoticons; /** * * 表情工廠類 載入表情種類 * */ public class SimpleCommonUtils { public static void initEmoticonsEditText(EmoticonsEditText etContent) { etContent.addEmoticonFilter(new EmojiFilter()); etContent.addEmoticonFilter(new XhsFilter()); } public static EmoticonClickListener getCommonEmoticonClickListener(final EditText editText) { return new EmoticonClickListener() { @Override public void onEmoticonClick(Object o, int actionType, boolean isDelBtn) { if (isDelBtn) { SimpleCommonUtils.delClick(editText); } else { if (o == null) { return; } if (actionType == Constants.EMOTICON_CLICK_TEXT) { String content = null; if (o instanceof EmojiBean) { content = ((EmojiBean) o).emoji; } else if (o instanceof EmoticonEntity) { content = ((EmoticonEntity) o).getContent(); } if (TextUtils.isEmpty(content)) { return; } int index = editText.getSelectionStart(); Editable editable = editText.getText(); editable.insert(index, content); } } } }; } public static PageSetAdapter sCommonPageSetAdapter; public static PageSetAdapter getCommonAdapter(Context context, EmoticonClickListener emoticonClickListener) { if(sCommonPageSetAdapter != null){ return sCommonPageSetAdapter; } PageSetAdapter pageSetAdapter = new PageSetAdapter(); //原生的笑臉表情 // addEmojiPageSetEntity(pageSetAdapter, context, emoticonClickListener); //QQ笑臉表情 addQqPageSetEntity(pageSetAdapter, context, emoticonClickListener); //龜頭表情 // addXhsPageSetEntity(pageSetAdapter, context, emoticonClickListener); //兔斯基 // addWechatPageSetEntity(pageSetAdapter, context, emoticonClickListener); //好好學習表情包 // addGoodGoodStudyPageSetEntity(pageSetAdapter, context, emoticonClickListener); //顏文字 addKaomojiPageSetEntity(pageSetAdapter, context, emoticonClickListener); // addTestPageSetEntity(pageSetAdapter, context); return pageSetAdapter; } /** * 插入emoji表情集 * * @param pageSetAdapter * @param context * @param emoticonClickListener */ public static void addEmojiPageSetEntity(PageSetAdapter pageSetAdapter, Context context, final EmoticonClickListener emoticonClickListener) { ArrayList<EmojiBean> emojiArray = new ArrayList<>(); Collections.addAll(emojiArray, DefEmoticons.sEmojiArray); EmoticonPageSetEntity emojiPageSetEntity = new EmoticonPageSetEntity.Builder() .setLine(3) .setRow(7) .setEmoticonList(emojiArray) .setIPageViewInstantiateItem(getDefaultEmoticonPageViewInstantiateItem(new EmoticonDisplayListener<Object>() { @Override public void onBindView(int position, ViewGroup parent, EmoticonsAdapter.ViewHolder viewHolder, Object object, final boolean isDelBtn) { final EmojiBean emojiBean = (EmojiBean) object; if (emojiBean == null && !isDelBtn) { return; } viewHolder.ly_root.setBackgroundResource(com.keyboard.view.R.drawable.bg_emoticon); if (isDelBtn) { viewHolder.iv_emoticon.setImageResource(R.drawable.icon_del); } else { viewHolder.iv_emoticon.setImageResource(emojiBean.icon); } viewHolder.rootView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (emoticonClickListener != null) { emoticonClickListener.onEmoticonClick(emojiBean, Constants.EMOTICON_CLICK_TEXT, isDelBtn); } } }); } })) .setShowDelBtn(EmoticonPageEntity.DelBtnStatus.LAST) .setIconUri(ImageBase.Scheme.DRAWABLE.toUri("icon_emoji")) .build(); pageSetAdapter.add(emojiPageSetEntity); } public static void addQqPageSetEntity(PageSetAdapter pageSetAdapter, Context context, final EmoticonClickListener emoticonClickListener) { EmoticonPageSetEntity kaomojiPageSetEntity = new EmoticonPageSetEntity.Builder() .setLine(3) .setRow(7) .setEmoticonList(ParseDataUtils.ParseQqData(DefQqEmoticons.sQqEmoticonHashMap)) .setIPageViewInstantiateItem(new PageViewInstantiateListener<EmoticonPageEntity>() { @Override public View instantiateItem(ViewGroup container, int position, EmoticonPageEntity pageEntity) { if (pageEntity.getRootView() == null) { EmoticonPageView pageView = new EmoticonPageView(container.getContext()); pageView.setNumColumns(pageEntity.getRow()); pageEntity.setRootView(pageView); try { EmoticonsAdapter adapter = new EmoticonsAdapter(container.getContext(), pageEntity, emoticonClickListener); adapter.setItemHeightMaxRatio(1.8); adapter.setOnDisPlayListener(getEmoticonDisplayListener(emoticonClickListener)); pageView.getEmoticonsGridView().setAdapter(adapter); } catch (Exception e) { e.printStackTrace(); } } return pageEntity.getRootView(); } }) .setShowDelBtn(EmoticonPageEntity.DelBtnStatus.LAST) .setIconUri(ImageBase.Scheme.DRAWABLE.toUri("kys")) .build(); pageSetAdapter.add(kaomojiPageSetEntity); } public static EmoticonDisplayListener<Object> getEmoticonDisplayListener(final EmoticonClickListener emoticonClickListener){ return new EmoticonDisplayListener<Object>() { @Override public void onBindView(int position, ViewGroup parent, EmoticonsAdapter.ViewHolder viewHolder, Object object, final boolean isDelBtn) { final EmoticonEntity emoticonEntity = (EmoticonEntity) object; if (emoticonEntity == null && !isDelBtn) { return; } viewHolder.ly_root.setBackgroundResource(com.keyboard.view.R.drawable.bg_emoticon); if (isDelBtn) { viewHolder.iv_emoticon.setImageResource(R.drawable.icon_del); } else { try { ImageLoader.getInstance(viewHolder.iv_emoticon.getContext()).displayImage(emoticonEntity.getIconUri(), viewHolder.iv_emoticon); } catch (IOException e) { e.printStackTrace(); } } viewHolder.rootView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (emoticonClickListener != null) { emoticonClickListener.onEmoticonClick(emoticonEntity, Constants.EMOTICON_CLICK_TEXT, isDelBtn); } } }); } }; } /** * 插入xhs表情集 * * @param pageSetAdapter * @param context * @param emoticonClickListener */ public static void addXhsPageSetEntity(PageSetAdapter pageSetAdapter, Context context, EmoticonClickListener emoticonClickListener) { EmoticonPageSetEntity xhsPageSetEntity = new EmoticonPageSetEntity.Builder() .setLine(3) .setRow(7) .setEmoticonList(ParseDataUtils.ParseXhsData(DefXhsEmoticons.xhsEmoticonArray, ImageBase.Scheme.ASSETS)) .setIPageViewInstantiateItem(getDefaultEmoticonPageViewInstantiateItem(getCommonEmoticonDisplayListener(emoticonClickListener, Constants.EMOTICON_CLICK_TEXT))) .setShowDelBtn(EmoticonPageEntity.DelBtnStatus.LAST) .setIconUri(ImageBase.Scheme.ASSETS.toUri("xhsemoji_19.png")) .build(); pageSetAdapter.add(xhsPageSetEntity); } /** * 插入微信表情集 * * @param pageSetAdapter * @param context * @param emoticonClickListener */ public static void addWechatPageSetEntity(PageSetAdapter pageSetAdapter, Context context, EmoticonClickListener emoticonClickListener) { String filePath = FileUtils.getFolderPath("wxemoticons"); EmoticonPageSetEntity<EmoticonEntity> emoticonPageSetEntity = ParseDataUtils.parseDataFromFile(context, filePath, "wxemoticons.zip", "wxemoticons.xml"); if (emoticonPageSetEntity == null) { return; } EmoticonPageSetEntity pageSetEntity = new EmoticonPageSetEntity.Builder() .setLine(emoticonPageSetEntity.getLine()) .setRow(emoticonPageSetEntity.getRow()) .setEmoticonList(emoticonPageSetEntity.getEmoticonList()) .setIPageViewInstantiateItem(getEmoticonPageViewInstantiateItem(BigEmoticonsAdapter.class, emoticonClickListener)) .setIconUri(ImageBase.Scheme.FILE.toUri(filePath + "/" + emoticonPageSetEntity.getIconUri())) .build(); pageSetAdapter.add(pageSetEntity); } /** * 插入我們愛學習表情集 * * @param pageSetAdapter * @param context * @param emoticonClickListener */ public static void addGoodGoodStudyPageSetEntity(PageSetAdapter pageSetAdapter, Context context, EmoticonClickListener emoticonClickListener) { String filePath = FileUtils.getFolderPath("goodgoodstudy"); EmoticonPageSetEntity<EmoticonEntity> emoticonPageSetEntity = ParseDataUtils.parseDataFromFile(context, filePath, "goodgoodstudy.zip", "goodgoodstudy.xml"); if (emoticonPageSetEntity == null) { return; } EmoticonPageSetEntity pageSetEntity = new EmoticonPageSetEntity.Builder() .setLine(emoticonPageSetEntity.getLine()) .setRow(emoticonPageSetEntity.getRow()) .setEmoticonList(emoticonPageSetEntity.getEmoticonList()) .setIPageViewInstantiateItem(getEmoticonPageViewInstantiateItem(BigEmoticonsAndTitleAdapter.class, emoticonClickListener)) .setIconUri(ImageBase.Scheme.FILE.toUri(filePath + "/" + emoticonPageSetEntity.getIconUri())) .build(); pageSetAdapter.add(pageSetEntity); } /** * 插入顏文字表情集 * * @param pageSetAdapter * @param context