1. 程式人生 > >安卓開發-最簡單快速的仿微信聊天實現-附贈微信原生表情,QQ原生表情

安卓開發-最簡單快速的仿微信聊天實現-附贈微信原生表情,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" />
<!--自定義toolbartitle 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