基于环信的仿QQ即时通讯的简单实现

我的博客地址

之前一直想实现聊天的功能,但是感觉有点困难,今天看了环信的API,就利用下午的时间动手试了试,然后做了一个小Demo。

因为没有刻意去做聊天软件,花的时间也不多,然后界面就很简单,都是一些基本知识,如果觉得功能简单,可以自行添加,我这就不多介绍了。

照例先来一波动态演示:


环信及时聊天.gif

功能很简单,注册用户 --> 用户登录 --> 选择聊天对象 --> 开始聊天

使用到的知识点:

  1. RecyclerView
  2. CardView
  3. 环信的API的简单使用

依赖的库

compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:cardview-v7:24.1.1'
compile 'com.android.support:recyclerview-v7:24.0.0'    

1、聊天页面

首先是看了郭神的《第二行代码》做了聊天界面,用的是RecyclerView

a. 消息类的封装


public class MSG {
    public static final int TYPE_RECEIVED = 0;//消息的类型:接收
    public static final int TYPE_SEND = 1;    //消息的类型:发送

    private String content;//消息的内容
    private int type;      //消息的类型

    public MSG(String content, int type) {
        this.content = content;
        this.type = type;
    }

    public String getContent() {
        return content;
    }

    public int getType() {
        return type;
    }
}

b. RecyclerView子项的布局


<LinearLayout
    android:id="@+id/ll_msg_left"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    <!-- 设置点击效果为水波纹(5.0以上) -->
    android:background="?android:attr/selectableItemBackground"
    android:clickable="true"
    android:focusable="true"
    android:orientation="horizontal"
    android:padding="2dp">

    <android.support.v7.widget.CardView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:cardCornerRadius="20dp"
        app:cardPreventCornerOverlap="false"
        app:cardUseCompatPadding="true">

        <ImageView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:scaleType="centerCrop"
            android:src="@mipmap/man" />
    </android.support.v7.widget.CardView>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/message_left"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tv_msg_left"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="10dp"
            android:textColor="#fff" />
    </LinearLayout>

</LinearLayout>

这是左边的部分,至于右边应该也就简单了。我用CardView把ImageView包裹起来,这样比较好看。效果如下:

item布局.png

c. RecyclerView适配器


    public class MsgAdapter extends RecyclerView.Adapter<MsgAdapter.MyViewHolder> {

    private List<MSG> mMsgList;

    public MsgAdapter(List<MSG> mMsgList) {
        this.mMsgList = mMsgList;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = View.inflate(parent.getContext(), R.layout.item_msg, null);
        MyViewHolder holder = new MyViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        MSG msg = mMsgList.get(position);
        if (msg.getType() == MSG.TYPE_RECEIVED){
            //如果是收到的消息,显示左边布局,隐藏右边布局
            holder.llLeft.setVisibility(View.VISIBLE);
            holder.llRight.setVisibility(View.GONE);
            holder.tv_Left.setText(msg.getContent());
        } else if (msg.getType() == MSG.TYPE_SEND){
            //如果是发送的消息,显示右边布局,隐藏左边布局
            holder.llLeft.setVisibility(View.GONE);
            holder.llRight.setVisibility(View.VISIBLE);
            holder.tv_Right.setText(msg.getContent());
        }
    }

    @Override
    public int getItemCount() {
        return mMsgList.size();
    }

    static class MyViewHolder extends RecyclerView.ViewHolder{

        LinearLayout llLeft;
        LinearLayout llRight;

        TextView tv_Left;
        TextView tv_Right;

        public MyViewHolder(View itemView) {
            super(itemView);

            llLeft = (LinearLayout) itemView.findViewById(R.id.ll_msg_left);
            llRight = (LinearLayout) itemView.findViewById(R.id.ll_msg_right);

            tv_Left = (TextView) itemView.findViewById(R.id.tv_msg_left);
            tv_Right = (TextView) itemView.findViewById(R.id.tv_msg_right);

        }
    }
}

这部分应该也没什么问题,就是适配器的创建,我之前的文章也讲过 传送门:简单粗暴----RecyclerView

d. RecyclerView初始化


就是一些基本的初始化,我就不赘述了,讲一下添加数据的细节处理

    btSend.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            String content = etInput.getText().toString().trim();
            if (!TextUtils.isEmpty(content)){

                ...//环信部分的发送消息

                MSG msg = new MSG(content, MSG.TYPE_SEND);
                mList.add(msg);
                //当有新消息时,刷新RecyclerView中的显示
                mAdapter.notifyItemInserted(mList.size() - 1);
                //将RecyclerView定位到最后一行
                mRecyclerView.scrollToPosition(mList.size() - 1);
                etInput.setText("");
            }
        }
    });

至此界面已经结束了,接下来就是数据的读取

2. 环信API的简单应用

官网有详细的API介绍 环信及时通讯V3.0,我这里就简单介绍如何简单集成

a. 环信开发账号的注册


环信官网

创建应用得到Appkey后面要用


环信注册.PNG

b. SDK导入


你可以直接下载然后拷贝工程的libs目录下

Android Studio可以直接添加依赖

将以下代码放到项目根目录的build.gradle文件里

repositories {
     maven { url "https://raw.githubusercontent.com/HyphenateInc/Hyphenate-SDK-Android/master/repository" }
}

在你的module的build.gradle里加入以下代码

android {
    //use legacy for android 6.0
    useLibrary 'org.apache.http.legacy'
}
dependencies {
    compile 'com.android.support:appcompat-v7:23.4.0'
    //Optional compile for GCM (Google Cloud Messaging).
    compile 'com.google.android.gms:play-services-gcm:9.4.0'
    compile 'com.hyphenate:hyphenate-sdk:3.2.3'
}

如果想使用不包含音视频通话的sdk,用compile 'com.hyphenate:hyphenate-sdk-lite:3.2.3'

c. 清单文件配置


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="Your Package"
    android:versionCode="100"
    android:versionName="1.0.0">

    <!-- Required -->
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>  
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
  
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:name="Your Application">
   
    <!-- 设置环信应用的AppKey -->
        <meta-data android:name="EASEMOB_APPKEY"  android:value="Your AppKey" />
        <!-- 声明SDK所需的service SDK核心功能-->
        <service android:name="com.hyphenate.chat.EMChatService" android:exported="true"/>
        <service android:name="com.hyphenate.chat.EMJobService"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="true"
            />
        <!-- 声明SDK所需的receiver -->
        <receiver android:name="com.hyphenate.chat.EMMonitorReceiver">
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_REMOVED"/>
                <data android:scheme="package"/>
            </intent-filter>
            <!-- 可选filter -->
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <action android:name="android.intent.action.USER_PRESENT" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

APP打包混淆

-keep class com.hyphenate.** {*;}
-dontwarn  com.hyphenate.**

d. 初始化SDK


在自定义Application的onCreate中初始化

public class MyApplication extends Application {

    private Context appContext;

    @Override
    public void onCreate() {
        super.onCreate();
        EMOptions options = new EMOptions();
        options.setAcceptInvitationAlways(false);

        appContext = this;
        int pid = android.os.Process.myPid();
        String processAppName = getAppName(pid);
        // 如果APP启用了远程的service,此application:onCreate会被调用2次
        // 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
        // 默认的APP会在以包名为默认的process name下运行,如果查到的process name不是APP的process name就立即返回

        if (processAppName == null || !processAppName.equalsIgnoreCase(appContext.getPackageName())) {
            Log.e("--->", "enter the service process!");

            // 则此application::onCreate 是被service 调用的,直接返回
            return;
        }

        //初始化
        EMClient.getInstance().init(getApplicationContext(), options);
        //在做打包混淆时,关闭debug模式,避免消耗不必要的资源
        EMClient.getInstance().setDebugMode(true);
    }

    private String getAppName(int pID) {
        String processName = null;
        ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
        List l = am.getRunningAppProcesses();
        Iterator i = l.iterator();
        PackageManager pm = this.getPackageManager();
        while (i.hasNext()) {
            ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
            try {
                if (info.pid == pID) {
                    processName = info.processName;
                    return processName;
                }
            } catch (Exception e) {
                // Log.d("Process", "Error>> :"+ e.toString());
            }
        }
        return processName;
    }
}

e. 注册和登陆


注册要在子线程中执行

//注册失败会抛出HyphenateException
EMClient.getInstance().createAccount(username, pwd);//同步方法

EMClient.getInstance().login(userName,password,new EMCallBack() {//回调
    @Override
    public void onSuccess() {
        EMClient.getInstance().groupManager().loadAllGroups();
        EMClient.getInstance().chatManager().loadAllConversations();
            Log.d("main", "登录聊天服务器成功!");        
    }
 
    @Override
    public void onProgress(int progress, String status) {
 
    }
 
    @Override
    public void onError(int code, String message) {
        Log.d("main", "登录聊天服务器失败!");
    }
});

f. 发送消息


//创建一条文本消息,content为消息文字内容,toChatUsername为对方用户或者群聊的id,后文皆是如此
EMMessage message = EMMessage.createTxtSendMessage(content, toChatUsername);
//发送消息
EMClient.getInstance().chatManager().sendMessage(message);

g. 接收消息


msgListener = new EMMessageListener() {

        @Override
        public void onMessageReceived(List<EMMessage> messages) {
            //收到消息
            String result = messages.get(0).getBody().toString();
            String msgReceived = result.substring(5, result.length() - 1);

            Log.i(TAG, "onMessageReceived: " + msgReceived);
            final MSG msg = new MSG(msgReceived, MSG.TYPE_RECEIVED);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mList.add(msg);
                    mAdapter.notifyDataSetChanged();
                    mRecyclerView.scrollToPosition(mList.size() - 1);
                }
            });
        }

        @Override
        public void onCmdMessageReceived(List<EMMessage> messages) {
            //收到透传消息
        }

        @Override
        public void onMessageRead(List<EMMessage> list) {

        }

        @Override
        public void onMessageDelivered(List<EMMessage> list) {

        }

        @Override
        public void onMessageChanged(EMMessage message, Object change) {
            //消息状态变动
        }
    };

接收消息的监听器分别需要在OnResume()和OnDestory()方法中注册和取消注册

EMClient.getInstance().chatManager().addMessageListener(msgListener);//注册

EMClient.getInstance().chatManager().removeMessageListener(msgListener);//取消注册

需要注意的是,当接收到消息,需要在主线程中更新适配器,否则会不能及时刷新出来

到此,一个简单的及时聊天Demo已经完成,功能很简单,如果需要添加额外功能的话,可以自行参考官网,官网给出的教程还是很不错的!

最后希望大家能多多支持我,需要你们的支持喜欢!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容