最近项目中集成即时聊天功能,挑来拣去,最终选择环信SDK来进行开发,选择环信的主要原因是接口方便、简洁,说明文档清晰易懂。文档有Android、iOS、和后台服务器端,还是非常全的。
环信官网:http://www.easemob.com/
本篇文章目的主要在于说明环信Demo如何实现即时通信的。我在集成环信SDK到我们自己开发的app之前,研究了一下环信demo的代码,看了两三天的样子,基本搞清楚来龙去脉,但是只是清楚来龙去脉,要说到里面的细节可能得深一步研究,但是这就够了,已经可以把demo里面的功能集成到我们自己的app中了。所以本篇文章就说明一下如何集成环信到自己的app中。
集成起来还是比较快的,最多一周时间集成就搞定了。我们是有自己的用户体系的,所以我们采用的是将环信与现有的APP用户体系集成。
集成之前,必然要到上面这个页面进行了解,如何集成,在这里说明了如何集成的方案,这个方案的选择就需要你自己根据已有的需求进行选择了。这个就不多说了,应该都明白。
登 录 原 理
我们的方案是将环信与现有的APP用户体系集成!也就是说我们的服务器需要把现有的用户在后台注册到环信服务器中,然后app登录的时候自动登录环信服务器,然后使用环信的即时通信功能。
这就意味着用户登录app的时候,需要登录两次,一次是我们的应用服务器,一次是环信服务器,只不过给用户的感觉是登录了一次,而环信服务器的登录是代码中控制的,用户看不到也感觉不到。
好友体系原理
登录之后,就是获取好友和群组了,环信增加了聊天室的功能,有点类似于松群组的功能,只不过聊天室更加随意些。群组大家都明白,不多说,聊天室呢不同,开放的公共的聊天室,成员可以随时进入聊天随时离开,离开之后自动不再收到聊天信息。
好友体系中环信是可以进行管理的,当然也可以不使用环信的好友管理体系,而使用应用服务器来进行好友的管理工作。我们项目中使用的是环信的好友管理体系,主要是方便,不过也不见得省了多少事儿,因为应用服务器用户体系的变更,都要由服务器把该用户体系的关系的变更通知环信服务器,然环信服务器也进行更改,从而保持应用服务器和环信服务器用户体系的一致性。所以大家集成过程中需要自己考虑代价。我们项目中使用环信管理好友体系主要在于app端方便,app端也不进行用户体系的变更,复杂的操作都在服务器端实现,所以app端方便实现、开发简单。
用户昵称、头像
环信服务器采用了低浸入的方式开发即时通信,也就是说它不保存用户的信息,也不访问用户的信息,这就意味着用户的昵称、头像等等信息环信是没有保存的,开发者无法通过环信获取用户信息。所以环信专门对与用户的昵称、头像信息给出了解决方案。
方法一 从APP服务器获取昵称和头像
方法二 从消息扩展中获取昵称和头像
昵称或头像处理的方法一和方法二区别:
方法一:在发送消息时不含有任何扩展,收消息时如果本地不存在发送人的用户信息则需要从APP服务器查询发送人的昵称和头像的URL。
方法二:在发送消息时带有包含昵称和头像URL的消息扩展,收到消息时即可从消息扩展中取出,不需要再去APP服务器获取, 方法二和方法一相比
优点:收到消息立即显示昵称不用等待APP服务器返回数据后显示。
缺点:每条消息都要带有扩展,增加消息体积,每次发消息都有一些不必要的数据。
上面是环信给出的用户昵称和头像的两种解决方案。这两种解决方案大家一看就应用明白了,不多说。主要说说我们项目中的解决方案,采用第一种方案,从应用服务器获取,保存本地数据库,之后,查询操作就是本地操作,那就会有问题了,用户关系更新或者信息更新呢?这个问题主要解决方法是用户好友体系的每次更新都会同时更新用户昵称和头像,然后更新本地数据库来解决这个问题。
到此,这三个问题明白之后,基本就可以开始进行开发了,你可能会说,还没有说明即时通信呢?最主要的就是即时通信怎么没有说明呢?这个问题大家勿急,后面会有!^_^
开 发
开发过程,首先就是要研究一下环信demo的代码,里面已经进行了封装,所以把环信demo的代码看懂,利用的好的代码完全可以应用到现有的app中。
这个环信demo的代码,导入手机直接运行,注册,用着非常好,代码运行正常,功能也正常,所以研究这个代码之后,再集成到自己的app中那就so easy!!
demo里面用到了几个jar包,主要是环信的sdk、百度地图、友盟数据分析、百度地图定位、图片加载等这几个jar包,百度地图这个应该没什么说的,之前我们app里面集成过,不过有点旧,这次顺带着把百度地图也更新成最新的了,目前百度地图最新的挺好用的。也算是教训,就是实时更新所应用的第三方的jar!别的jar就没什么说的了。
下面就是demo里面的分包了,demo里面的分包比较多,不过从分包的名字可以看出每个包下面的代码是什么作用了。我主要看的是activity包下面的每个类,因为activity类就是一个个的界面,其他的都是为这个activity类服务的代码工具类,所以主要看这个就可以了。
activity包下面的类比较多,不过我们关心的类只有几个而已,ChatActivity.Java类就是即时聊天的界面,这个一定是要集成到自己的app当中的。其他的三个ContactlistFragment.java、ChatAllHistoryFragment.java、GroupsActivity.java这三个类分别是联系人界面、回话历史界面、群组界面。这三个需要根据自己app的需求进行集成。所以主要研究的工作就是放在这几个类上。
MainActivity.java就是主界面,主界面集成了上面三个界面,由主界面进行管理界面的显示。
剩下的工作没什么特别的了,搞不明白代码的可以给我留言,相互交流一下。
特别提一下下面的几个类
这个几个类有点绕!刚开始着实弄混了。现在看来demo里面代码也是用心良苦呀!!
1、先看controller包下面的HXSDKHelper.java类,再看chatuidemo包下面的DemoHXSDKHelper.java类,明显是继承关系!后者才是demo中使用的对象类。并且该父类在controller包下,明显是控制信息管理类,打开该类查看代码
从说明可以看出该类的作用了。
2、再看HXSDKModel.java类,这类名字就是模版类,还有DefaultHXSDKModel.java类和DemoSDKModel.java类,也很明显存在继承关系。完成的功能主要是app当中即时通信的一些数据的保存和控制信息显示信息等。
这几个类搞清楚之后基本就没有什么打的问题了。
主要代码讲解
1、主类MainActivity.java
public class MainActivity extends BaseActivity implements EMEventListener
该类实现了EMEventListener 接口,就一个方法如下:
/** * 监听事件 */ @Override public void onEvent(EMNotifierEvent event) { switch (event.getEvent()) { case EventNewMessage: // 普通消息 { EMMessage message = (EMMessage) event.getData(); // 提示新消息 HXSDKHelper.getInstance().getNotifier().onNewMsg(message); refreshUI(); break; } case EventOfflineMessage: { refreshUI(); break; } case EventConversationListChanged: { refreshUI(); break; } default: break; } }
主要就是监听新消息、离线消息、回话消息变化等,然后更新界面refreshUI(),更新界面就是刷新未读消息数、刷新联系人列表,回话列表等。
在主界面初始化中注册了三个监听器,如下代码:
private void init() { // setContactListener监听联系人的变化等 EMContactManager.getInstance().setContactListener(new MyContactListener()); // 注册一个监听连接状态的listener connectionListener = new MyConnectionListener(); EMChatManager.getInstance().addConnectionListener(connectionListener); groupChangeListener = new MyGroupChangeListener(); // 注册群聊相关的listener }
这三个监听器就是监听联系人变化、群组变化、与环信服务器链接变化的监听器,这三者的变化都会回调这三个监听器里面的相应的方法,方便开发者通过相应的方法采取相应的措施。
这三个监听器demo中代码比较详细,在此就不多说了。
2 联系人列表ContactlistFragment.java类
/** * 联系人列表页 */ public class ContactlistFragment extends Fragment { public static final String TAG = "ContactlistFragment"; private ContactAdapter adapter; private ListcontactList; private ListView listView; private boolean hidden; private Sidebar sidebar; private InputMethodManager inputMethodManager; private List blackList; ImageButton clearSearch; EditText query; HXContactSyncListener contactSyncListener; HXBlackListSyncListener blackListSyncListener; View progressBar; Handler handler = new Handler(); private User toBeProcessUser; private String toBeProcessUsername; /** * 这里注册了两个监听器,目的在于同步联系人信息 * 当联系人发生变化、黑名单发生变化,通知这里注册的监听器 * 进而刷新界面 * */ class HXContactSyncListener implements HXSDKHelper.HXSyncListener { @Override public void onSyncSucess(final boolean success) { EMLog.d(TAG, "on contact list sync success:" + success); ContactlistFragment.this.getActivity().runOnUiThread(new Runnable() { public void run() { getActivity().runOnUiThread(new Runnable(){ @Override public void run() { if(success){ progressBar.setVisibility(View.GONE); refresh(); }else{ String s1 = getResources().getString(R.string.get_failed_please_check); Toast.makeText(getActivity(), s1, 1).show(); progressBar.setVisibility(View.GONE); } } }); } }); } } class HXBlackListSyncListener implements HXSyncListener{ @Override public void onSyncSucess(boolean success) { getActivity().runOnUiThread(new Runnable(){ @Override public void run() { blackList = EMContactManager.getInstance().getBlackListUsernames(); refresh(); } }); } }; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_contact_list, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); //防止被T后,没点确定按钮然后按了home键,长期在后台又进app导致的crash if(savedInstanceState != null && savedInstanceState.getBoolean("isConflict", false)) return; inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); listView = (ListView) getView().findViewById(R.id.list); sidebar = (Sidebar) getView().findViewById(R.id.sidebar); sidebar.setListView(listView); //黑名单列表 blackList = EMContactManager.getInstance().getBlackListUsernames(); contactList = new ArrayList (); // 获取设置contactlist getContactList(); //搜索框 query = (EditText) getView().findViewById(R.id.query); query.setHint(R.string.search); clearSearch = (ImageButton) getView().findViewById(R.id.search_clear); query.addTextChangedListener(new TextWatcher() { public void onTextChanged(CharSequence s, int start, int before, int count) { adapter.getFilter().filter(s); if (s.length() > 0) { clearSearch.setVisibility(View.VISIBLE); } else { clearSearch.setVisibility(View.INVISIBLE); } } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void afterTextChanged(Editable s) { } }); clearSearch.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { query.getText().clear(); hideSoftKeyboard(); } }); // 设置adapter adapter = new ContactAdapter(getActivity(), R.layout.row_contact, contactList); listView.setAdapter(adapter); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView> parent, View view, int position, long id) { String username = adapter.getItem(position).getUsername(); if (Constant.NEW_FRIENDS_USERNAME.equals(username)) { // 进入申请与通知页面 User user = DemoApplication.getInstance().getContactList().get(Constant.NEW_FRIENDS_USERNAME); user.setUnreadMsgCount(0); startActivity(new Intent(getActivity(), NewFriendsMsgActivity.class)); } else if (Constant.GROUP_USERNAME.equals(username)) { // 进入群聊列表页面 startActivity(new Intent(getActivity(), GroupsActivity.class)); } else if(Constant.CHAT_ROOM.equals(username)){ //进入聊天室列表页面 startActivity(new Intent(getActivity(), PublicChatRoomsActivity.class)); }else { // demo中直接进入聊天页面,实际一般是进入用户详情页 startActivity(new Intent(getActivity(), ChatActivity.class).putExtra("userId", adapter.getItem(position).getUsername())); } } }); listView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // 隐藏软键盘 if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) { if (getActivity().getCurrentFocus() != null) inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } return false; } }); ImageView addContactView = (ImageView) getView().findViewById(R.id.iv_new_contact); // 进入添加好友页 addContactView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(getActivity(), AddContactActivity.class)); } }); registerForContextMenu(listView); progressBar = (View) getView().findViewById(R.id.progress_bar); contactSyncListener = new HXContactSyncListener(); HXSDKHelper.getInstance().addSyncContactListener(contactSyncListener); blackListSyncListener = new HXBlackListSyncListener(); HXSDKHelper.getInstance().addSyncBlackListListener(blackListSyncListener); if (!HXSDKHelper.getInstance().isContactsSyncedWithServer()) { progressBar.setVisibility(View.VISIBLE); } else { progressBar.setVisibility(View.GONE); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); if (((AdapterContextMenuInfo) menuInfo).position > 2) { toBeProcessUser = adapter.getItem(((AdapterContextMenuInfo) menuInfo).position); toBeProcessUsername = toBeProcessUser.getUsername(); getActivity().getMenuInflater().inflate(R.menu.context_contact_list, menu); } } @Override public boolean onContextItemSelected(MenuItem item) { if (item.getItemId() == R.id.delete_contact) { try { // 删除此联系人 deleteContact(toBeProcessUser); // 删除相关的邀请消息 InviteMessgeDao dao = new InviteMessgeDao(getActivity()); dao.deleteMessage(toBeProcessUser.getUsername()); } catch (Exception e) { e.printStackTrace(); } return true; }else if(item.getItemId() == R.id.add_to_blacklist){ moveToBlacklist(toBeProcessUsername); return true; } return super.onContextItemSelected(item); } /** * 当该Fragment对象改变了隐藏状态(由isHidden()方法返回)时,系统会调用这个方法。 * Fragment初始是不隐藏的,只要Fragment对象改变了它的显示状态,就会调用该方法。 * 参数hidden 如果该Fragment对象现在是隐藏的,则该参数是true,否则是false。 */ @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); this.hidden = hidden; if (!hidden) { refresh(); } } @Override public void onResume() { super.onResume(); if (!hidden) { refresh(); } } /** * 删除联系人 * * @param toDeleteUser */ public void deleteContact(final User tobeDeleteUser) { String st1 = getResources().getString(R.string.deleting); final String st2 = getResources().getString(R.string.Delete_failed); final ProgressDialog pd = new ProgressDialog(getActivity()); pd.setMessage(st1); pd.setCanceledOnTouchOutside(false); pd.show(); new Thread(new Runnable() { public void run() { try { EMContactManager.getInstance().deleteContact(tobeDeleteUser.getUsername()); // 删除db和内存中此用户的数据 UserDao dao = new UserDao(getActivity()); dao.deleteContact(tobeDeleteUser.getUsername()); DemoApplication.getInstance().getContactList().remove(tobeDeleteUser.getUsername()); getActivity().runOnUiThread(new Runnable() { public void run() { pd.dismiss(); adapter.remove(tobeDeleteUser); adapter.notifyDataSetChanged(); } }); } catch (final Exception e) { getActivity().runOnUiThread(new Runnable() { public void run() { pd.dismiss(); Toast.makeText(getActivity(), st2 + e.getMessage(), 1).show(); } }); } } }).start(); } /** * 把user移入到黑名单 */ private void moveToBlacklist(final String username){ final ProgressDialog pd = new ProgressDialog(getActivity()); String st1 = getResources().getString(R.string.Is_moved_into_blacklist); final String st2 = getResources().getString(R.string.Move_into_blacklist_success); final String st3 = getResources().getString(R.string.Move_into_blacklist_failure); pd.setMessage(st1); pd.setCanceledOnTouchOutside(false); pd.show(); new Thread(new Runnable() { public void run() { try { //加入到黑名单 EMContactManager.getInstance().addUserToBlackList(username,false); getActivity().runOnUiThread(new Runnable() { public void run() { pd.dismiss(); Toast.makeText(getActivity(), st2, 0).show(); refresh(); } }); } catch (EaseMobException e) { e.printStackTrace(); getActivity().runOnUiThread(new Runnable() { public void run() { pd.dismiss(); Toast.makeText(getActivity(), st3, 0).show(); } }); } } }).start(); } // 刷新ui public void refresh() { try { // 可能会在子线程中调到这方法 getActivity().runOnUiThread(new Runnable() { public void run() { getContactList(); adapter.notifyDataSetChanged(); } }); } catch (Exception e) { e.printStackTrace(); } } @Override public void onDestroy() { if (contactSyncListener != null) { HXSDKHelper.getInstance().removeSyncContactListener(contactSyncListener); contactSyncListener = null; } if(blackListSyncListener != null){ HXSDKHelper.getInstance().removeSyncBlackListListener(blackListSyncListener); } super.onDestroy(); } public void showProgressBar(boolean show) { if (progressBar != null) { if (show) { progressBar.setVisibility(View.VISIBLE); } else { progressBar.setVisibility(View.GONE); } } } /** * 获取联系人列表,并过滤掉黑名单和排序 */ private void getContactList() { contactList.clear(); //获取本地好友列表 Map users = DemoApplication.getInstance().getContactList(); Iterator > iterator = users.entrySet().iterator(); while (iterator.hasNext()) { Entry entry = iterator.next(); if (!entry.getKey().equals(Constant.NEW_FRIENDS_USERNAME) && !entry.getKey().equals(Constant.GROUP_USERNAME) && !entry.getKey().equals(Constant.CHAT_ROOM) && !blackList.contains(entry.getKey())){ EMLog.i(TAG, "获取联系人="+entry.getValue()); contactList.add(entry.getValue()); } } // 排序 Collections.sort(contactList, new Comparator () { @Override public int compare(User lhs, User rhs) { return lhs.getUsername().compareTo(rhs.getUsername()); } }); // 加入"群聊"和"聊天室" if(users.get(Constant.CHAT_ROOM) != null) contactList.add(0, users.get(Constant.CHAT_ROOM)); if(users.get(Constant.GROUP_USERNAME) != null) contactList.add(0, users.get(Constant.GROUP_USERNAME)); // 把"申请与通知"添加到首位 if(users.get(Constant.NEW_FRIENDS_USERNAME) != null) contactList.add(0, users.get(Constant.NEW_FRIENDS_USERNAME)); } void hideSoftKeyboard() { if (getActivity().getWindow().getAttributes().softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) { if (getActivity().getCurrentFocus() != null) inputMethodManager.hideSoftInputFromWindow(getActivity().getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if(((MainActivity)getActivity()).isConflict){ outState.putBoolean("isConflict", true); }else if(((MainActivity)getActivity()).getCurrentAccountRemoved()){ outState.putBoolean(Constant.ACCOUNT_REMOVED, true); } } }
上面联系人类中的注册的监听器使用的就是观察者模式,先看HXSDKHelper.java中的部分代码
public void addSyncGroupListener(HXSyncListener listener) { if (listener == null) { return; } if (!syncGroupsListeners.contains(listener)) { syncGroupsListeners.add(listener); } } public void removeSyncGroupListener(HXSyncListener listener) { if (listener == null) { return; } if (syncGroupsListeners.contains(listener)) { syncGroupsListeners.remove(listener); } } public void addSyncContactListener(HXSyncListener listener) { if (listener == null) { return; } if (!syncContactsListeners.contains(listener)) { syncContactsListeners.add(listener); } } public void removeSyncContactListener(HXSyncListener listener) { if (listener == null) { return; } if (syncContactsListeners.contains(listener)) { syncContactsListeners.remove(listener); } } public void addSyncBlackListListener(HXSyncListener listener) { if (listener == null) { return; } if (!syncBlackListListeners.contains(listener)) { syncBlackListListeners.add(listener); } } public void removeSyncBlackListListener(HXSyncListener listener) { if (listener == null) { return; } if (syncBlackListListeners.contains(listener)) { syncBlackListListeners.remove(listener); } } public void noitifyGroupSyncListeners(boolean success){ for (HXSyncListener listener : syncGroupsListeners) { listener.onSyncSucess(success); } } public void notifyContactsSyncListener(boolean success){ for (HXSyncListener listener : syncContactsListeners) { listener.onSyncSucess(success); } } public void notifyBlackListSyncListener(boolean success){ for (HXSyncListener listener : syncBlackListListeners) { listener.onSyncSucess(success); } }
这部分代码控制着观察者,添加、删除、通知每一个观察者,当群组、好友、黑名单 通过环信服务器同步到客户端之后,notify每个观察者,然后观察者接收到之后,刷新UI。这里就是观察者模式的经典应用!!!
联系人列表看懂之后,其他的群组界面和回话历史界面就不多说了。
3 聊天界面ChatActivity.java
这个类比较庞大,因为demo里面把单聊和群聊、聊天室都集成到这一个界面中完成,代码很庞大,但是不影响最终的集成,直接集成该类就可以实现功能。不多说。
附上界面:
图一 回话历史界面
图二 通讯录界面好友
图三 设置界面
图四 聊天界面
最后附上源码下载
补充:
环信官方网站已经发布IM3.0版本。目前开发的一个app采用的就是IM3.0版本。
整体界面没发生大的变化,功能也都一样。但是在官方给的demo代码上优化很多,方便很多。不过得大概看懂里面的代码。如果是高手的话,半天就应该能集成好环信的即时通信功能。
本文给出的下载链接,是IM2.0版本。所以如果想要使用IM3.0的版本的,需要到官网下载。
对于新手来说,环信官网给出的demo是可以直接使用的。人家给出的是完整的app代码。新手就疑惑,不知道该如何入手集成即时通信功能?
其实很简单!
首先,把环信官网给出的依赖包和动态库添加到自己的工程中。目前官网给出的依赖包和动态库分为包含语音视频通话功能的和不包括语音视频通话功能的。大家根据自己的APP的功能添加。
然后,把demo里面的聊天界面直接复制到自己的功能里面,此时复制进去以后,会出现大量的错误!因为聊天界面关联了很多demo中的其他类,所以,要把其他类复制到自己的工程中。记得不要忘记布局文件、资源图片文件、字符串等等资源文件! 建议:在自己的工程中,新建一个包专门放环信的类。因为你要复制的类非常多!大概有二三十个!
最后,向即时通信代码填充数据。主要有几部分:
1)application类中环信helper类完成初始化操作。
2)登录app界面做好登录环信服务器操作。需要登录环信的登录名和密码。这里的环信登录与登录app 不同。APP登录是应用服务器的用户,用户名和密码在应用服务器。而登录环信是环信的登录名和密码, 需要先注册到环信服务器才行。注册操作可以在应用服务器提前做好。APP登录应用服务器的时候顺带着登录环信服务器即可。
3)获取好友信息。这里要分为好友信息的维护是应用服务器维护还是环信帮助你维护。这个我就不多说 了。环信官网有说明。
4)本地维护好友列表和聊天信息里列表。聊天信息列表在环信中已经不让开发者编辑和改变了。该功能 已经集成在了依赖的环信的包中了。好友列表在demo中给出了简单的数据表。开发者可以自己根据APP 需要开发和扩展。
5)退出APP。退出APP时务必调用helper类的logout方法。这样以后,先前登录的用户就从APP上退 出了环信的服务器。开发者要注意,我这里说的退出时指APP用户手动退出,不是用户按手机返回按钮或者返回主界面按钮导致APP退出。而是APP中的退出按钮,当前登录用户退出APP。如果是用户按返回按钮或回主界面按钮,返回到手机桌面的,没必要调用helper类的logout方法。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。