注册

先定一个小目标,比如写一个QQ

 项目简介 

   本项目是即时通讯的示例项目,使用了MVP模式,集成了环信SDK和Bmob后端云,展示了即时通讯基本功能的实现,包括注册登录,退出登录,联系人列表,添加好友,删除好友,收发消息,消息提醒等功能。
 
 使用的开源项目 
  学习目标 
  •  环信SDK的集成与使用
  •  MVP模式的运用
  •  ORM数据库的集成与使用
  •  模块化思想的运用
即时通讯 IM(Instant Messaging)
允许两人或多人使用网络即时的传递文字讯息、档案、语音与视频交流。
相关产品
  •  鼻祖 ICQ
  •  国内主流 QQ 微信 陌陌 YY等
  •  国外主流 Facebook Messenger WhatsApp Skype Instagram Line
第三方服务平台 环信 

001.png

环信集成[list=1]注册并创建应用下载SDKSDK的导入SDK初始化 so文件夹1. 放在jniLibs2. 也可以放在libs目录下,不过需要在模块下的配置文件中配置  
 android {     sourceSets {         main {             jniLibs.srcDirs = ['libs']         }     } }
注意事项运行出错:Didn't find class "com.hyphenate.chat.adapter.EMACallSession",原因是hyphenatechat_3.2.0.jar包内没有该类。解决办法:导入Demo源码中EaseUI库里面的hyphenatechat_3.2.0.jar替换。软件架构MVCMVC应用于Ruby on Rails, Spring Framework, iOS开发和 ASP.NET等。
  •  Model: 获取数据的业务逻辑,网络操作,数据库操作
  •  View: UI
  •  Controller: 操作Model层获取数据传递给UI

002.png

服务器端的MVC

003.png

Android中MVCAndroid中并没有清晰的MVC框架,如果把Activity当做Controller,根据我们实际开发经验,里面会有大量的UI操作,所以V和C就傻傻分不清了。
  •  Model:Java Bean, NetworkManager, DataBaseHelper
  •  View: xml res
  •  Controller: Activity Fragment
  •  ArrayList-ListView-Adapter(MVC)
MVPMVP主要应用于ASP.NET等。MVP与MVC主要区别是View和Model不再耦合。
  •  Model: 获取数据的业务逻辑,网络操作,数据库操作
  •  View: UI
  •  Presenter: 操作Model层获取数据传递给UI
 

004.png

MVVMMVVM主要应用于WPF, Silverlight, Caliburn, nRoute等。[list=1] Model: 获取数据的业务逻辑,网络操作,数据库操作 View: UI ViewModel: 将View和Model绑定

005.png

Android中MVVM 软件架构的核心思想
分层分模块

006.png

 参考 准备好了么? 开车啦!!! 包的创建
  •  adapter 存放适配器
  •  app 存放常量类,Application类以及一些app层级的全局类
  •  database 数据库相关类
  •  event EventBus使用的事件类
  •  factory 工厂类
  •  model 数据模型
  •  presenter MVP模型中的Presenter类
  •  ui 存放activity和fragment
  •  utils 工具类
  •  view MVP模型中的View类
  •  widget 自定义控件
基类的创建
  •  BaseActivity
  •  BaseFragment
Splash界面功能需求[list=1]如果没有登录,延时2s, 跳转到登录界面如果已经登录,则跳转到主界面MVP实现
  •  SplashView
  •  SplashPresenter
判断是否登录环信
@Overridepublic void checkLoginStatus() {    if (EMClient.getInstance().isLoggedInBefore() && EMClient.getInstance().isConnected()) {        mSplashView.onLoggedIn();    } else {        mSplashView.onNotLogin();    }}
 登录界面功能需求[list=1]点击登录按钮或者点击虚拟键盘上的Action键都能发起登录操作点击新用户,跳转到注册界面。MVP实现
  •  LoginView
  •  LoginPresenter
IME Options注意配置EditText的imeOptions属性时,需要配合inputType才能起作用。
android:imeOptions="actionNext"//下一个android:imeOptions="actionGo"//启动android:imeOptions="actionDone"//完成android:imeOptions="actionPrevious"//上一个android:imeOptions="actionSearch"//搜索android:imeOptions="actionSend"//发送
监听软键盘Action事件,发起登录 
private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {    @Override    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {        if (actionId == EditorInfo.IME_ACTION_GO) {            startLogin();            return true;        }        return false;    }};
EMCallBack的适配器     EMCallBack是环信的一个请求回调接口,包括请求成功的回调onSuccess,请求失败的回调onError和请求进度回调onProgress,但在实际使用过程中,通常只使用到请求成功和失败的回调,请求进度回调通常留在那里成了一个空方法,对于一个有代码洁癖的搬砖师来说,这是很难受的。所以我们可以创建一个适配器类实现这个接口,使用时用适配器类来替换EMCallBack接口,这样只需要覆写我们想覆写的方法就可以了。
//EMCallBack接口的适配器public class EMCallBackAdapter implements EMCallBack{    @Override    public void onSuccess() {    }    @Override    public void onError(int i, String s) {    }    @Override    public void onProgress(int i, String s) {    }}//EMCallBack适配器的使用private EMCallBackAdapter mEMCallBack = new EMCallBackAdapter() {    @Override    public void onSuccess() {        ThreadUtils.runOnUiThread(new Runnable() {            @Override            public void run() {                mLoginView.onLoginSuccess();            }        });    }    @Override    public void onError(int i, String s) {        ThreadUtils.runOnUiThread(new Runnable() {            @Override            public void run() {                mLoginView.onLoginFailed();            }        });    }};
Android6.0动态权限管理

009.png

Andrioid6.0对权限进行了分组,涉及到用户敏感信息的权限只能动态的去获取。当应用的targetSdkVersion小于23时,会默认采用以前的权限管理机制,当targetSdkVersion大于等于23时并且运行在Andorid6.0系统上,它才会采用这套新的权限管理机制。参考 动态获取写磁盘权限环信SDK内部维护了一个数据库来存储聊天记录等数据,需要读写磁盘的权限,所以我们在用户登录之前要申请该权限。类似的,很多同学在集成百度地图或者高德地图时常常不能正常定位,往往是忽略了在Android6.0上需要获取位置的权限。
/** * 是否有写磁盘权限 */private boolean hasWriteExternalStoragePermission() {    int result = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);    return result == PermissionChecker.PERMISSION_GRANTED;}/** * 申请权限 */private void applyPermission() {    String permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE};    ActivityCompat.requestPermissions(this, permissions, REQUEST_WRITE_EXTERNAL_STORAGE);}/** * 申请权限回调 */@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String permissions, @NonNull int grantResults) {    switch (requestCode) {        case REQUEST_WRITE_EXTERNAL_STORAGE:            if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) {                login();            } else {                toast(getString(R.string.not_get_permission));            }            break;    }}
注册界面

010.png


011.png

功能需求[list=1]用户名的长度必须是3-20位,首字母必须为英文字符,其他字符则除了英文外还可以是数字或者下划线。密码必须是3-20位的数字。密码和确认密码一致MVP实现
  • RegisterView
  • RegisterPresenter
正则表达式 
private static final String USER_NAME_REGEX = "^[a-zA-Z]\\w{2,19}$";//用户名的正则表达式private static final String PASSWORD_REGEX = "^[0-9]{3,20}$";//密码的正则表达式
  •  ^ 匹配输入字符串的开始位置
  • [a-zA-Z]     字符范围。匹配指定范围内的任意字符。
  • \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
  •  $ 匹配输入字符串的结束位置
 注册流程[list=1] 实际项目中,注册会将用户名和密码注册到APP的服务器,然后APP的服务器再通过REST API方式注册到环信服务器。 由于本项目没有APP服务器,会将用户数据注册到第三方云数据库Bmob,注册成功后,再在客户端发送请求注册到环信服务器。

012.png

云数据库 Bmob集成Bmob开发文档 
  •  注册创建应用
  •  下载SDK
  •  导入SDK
  •  初始化SDk
隐藏软键盘在点击注册按钮发起注册流程后,我们需要隐藏掉软键盘并且弹出进度条。
protected void hideSoftKeyboard() {    if (mInputMethodManager == null) {        mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);    }    mInputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);}
注册用户到BmobBmob用户管理
private void registerBmob(final String userName, final String pwd) {    User user = new User(userName, pwd);    user.signUp(new SaveListener() {        @Override        public void done(User user, BmobException e) {            if (e == null) {                registerEaseMob(userName, pwd);            } else {                notifyRegisterFailed(e);            }        }    });}
注册到环信
private void registerEaseMob(final String userName, final String pwd) {    ThreadUtils.runOnBackgroundThread(new Runnable() {        @Override        public void run() {            try {                EMClient.getInstance().createAccount(userName, pwd);                ThreadUtils.runOnUiThread(new Runnable() {                    @Override                    public void run() {                        mRegisterView.onRegisterSuccess();                    }                });            } catch (HyphenateException e) {                e.printStackTrace();                ThreadUtils.runOnUiThread(new Runnable() {                    @Override                    public void run() {                        mRegisterView.onRegisterError();                    }                });            }        }    });}
用户名已注册的处理当我们注册一个用户名到Bmob云数据库后,使用相同的用户名再次注册,Bmob会返回错误码202,信息为username '%s' already taken。所以我们应该处理该错误,通知用户换一个用户名注册。Bmob错误码  
private void notifyRegisterFailed(BmobException e) {    if (e.getErrorCode() == Constant.ErrorCode.USER_ALREADY_EXIST) {        mRegisterView.onResisterUserExist();    } else {        mRegisterView.onRegisterError();    }}
主界面

013.png

底部导航条 主界面底部是一个Tab导航条,实现的方法可以有很多种,我们可以使用RadioGroup或者FragmentTabHost, 或者我们可以自定义一个控件,为了不重复造轮子,本Demo使用了第三方导航条BottomBar。BottomBar的使用请参考其Github地址。第三方导航条Fragment的切换    
private OnTabSelectListener mOnTabSelectListener = new OnTabSelectListener() {    @Override    public void onTabSelected(@IdRes int tabId) {        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();        fragmentTransaction.replace(R.id.fragment_container, FragmentFactory.getInstance().getFragment(tabId)).commit();    }};
动态界面

014.png

MVP实现
  •  DynamicView
  •  DynamicPresenter
动态界面目前只实现了退出登录功能。
@Overridepublic void logout() {    mDynamicView.onStartLogout();    EMClient.getInstance().logout(true, mEMCallBackAdapter);}
联系人界面

015.png


016.jpg

MVP实现
  •  ContactView
  •  ContactPresenter
使用RecyclerView实现联系人列表RecyclerView类似ListView, 但比ListView更高级更灵活。使用RecyclerView必须指定一个适配器和一个布局管理器,经常有同学忘记设置布局管理器,结果导致RecyclerView什么也没有显示。适配器通常继承自RecyclerView.Adapter。RecyclerView提供这些内置管理器:
  •  LinearLayoutManager 线性布局管理器 (类似ListView效果)
  •  GridLayoutManager 网格布局管理器 (类似GridView效果)
  •  StaggeredGridLayoutManager 分散网格布局管理器(瀑布流效果)
如果要自定义一个布局管理器,请继承自RecyclerView.LayoutManager。另外,RecyclerView在默认情况下启用增添与删除item的动画,如果要定义这些动画,请继承RecyclerView.ItemAnimator并使用RecyclerView.setItemAnimator()。 初始化    
private void initRecyclerView() {    mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));//设置布局管理器    mRecyclerView.setHasFixedSize(true);//设置true如果列表内容不会影响RecyclerView的大小    mContactListAdapter = new ContactListAdapter(getContext(), mContactPresenter.getContactList());    mContactListAdapter.setOnItemClickListener(mOnItemClickListener);    mRecyclerView.setAdapter(mContactListAdapter);}
ContactListAdapter的实现 ContactListItemView

017.png

运用模块化的思想,将联系人的列表项抽取成一个独立的自定义组合式控件ContactListItemView。ContactListItemView只需传入一个与之对应的数据模型ContactListItem即可完成渲染。联系人是否在同一个组

018.png

当多个联系人的首字符相同时,只有第一个ContactListItemView显示首字符,后续首字符相同的ContactListItemView均不显示首字符。在ContactListItem中声明一个布尔型变量showFirstLetter来标记是否显示首字符。该变量在创建ContactListItem时赋值。
/** * 获取联系人列表数据 * @throws HyphenateException */private void startGetContactList() throws HyphenateException {    List contacts = EMClient.getInstance().contactManager().getAllContactsFromServer();    DatabaseManager.getInstance().deleteAllContacts();    if (!contacts.isEmpty()) {        for (int i = 0; i < contacts.size(); i++) {            ContactListItem item = new ContactListItem();            item.userName = contacts.get(i);            if (itemInSameGroup(i, item)) {                item.showFirstLetter = false;            }            mContactListItems.add(item);            saveContactToDatabase(item.userName);        }    }}/** * 当前联系人跟上个联系人比较,如果首字符相同则返回true * @param i 当前联系人下标 * @param item 当前联系人数据模型 * @return true 表示当前联系人和上一联系人在同一组 */private boolean itemInSameGroup(int i, ContactListItem item) {    return i > 0 && (item.getFirstLetter() == mContactListItems.get(i - 1).getFirstLetter());}
CardView的使用

019.png

CardView继承自FrameLayout, 是Material Design里面的卡片设计,带有圆角和阴影效果。CardView效果非常好看,但也不能滥用,比如:

020.png

更多的使用规范,请参考Material Design的设计规范Material Design Cards。这里我们给ContactListItemView的布局里面包了一层CardView,实际上是不可取的做法,这里只是为了展示CardView的使用姿势,实际项目中请大家遵循Material Design的设计规范。参考SwipeRefreshLayout的使用  
//设置SwipeRefreshLayout的颜色mSwipeRefreshLayout.setColorSchemeResources(R.color.qq_blue, R.color.qq_red);//设置SwipeRefreshLayout的监听器mSwipeRefreshLayout.setOnRefreshListener(mOnRefreshListener);
自定义控件SlideBar分类字符数组 
rivate static final String SECTIONS = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"        , "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
 绘制居中文本FontMetrics

021.png

@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    mTextSize = h * 1.0f / SECTIONS.length;//计算分配给字符的高度    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();    float mTextHeight = fontMetrics.descent - fontMetrics.ascent;//获取绘制字符的实际高度    mTextBaseline = mTextSize / 2 + mTextHeight/2 - fontMetrics.descent;//计算字符居中时的baseline}@Overrideprotected void onDraw(Canvas canvas) {    float x = getWidth() * 1.0f / 2;    float baseline = mTextBaseline;    for(int i = 0; i < SECTIONS.length; i++) {        canvas.drawText(SECTIONS[i], x, baseline, mPaint);        baseline += mTextSize;    }}[/i]
在ContactFragment里面监听SlideBar的事件 当用户在SlideBar上ACTION_DOWN和ACTION_MOVE时,如果用户触摸到的首字符发生变化,则会回调监听器onSectionChange,当ACTION_UP时回调onSlidingFinish()。
private SlideBar.OnSlideBarChangeListener mOnSlideBarChangeListener = new SlideBar.OnSlideBarChangeListener() {    @Override    public void onSectionChange(int index, String section) {        mSection.setVisibility(View.VISIBLE);//显示中间绿色背景的首字符        mSection.setText(section);        scrollToSection(section);//滚动RecyclerView到用户滑到的首字符    }    @Override    public void onSlidingFinish() {        mSection.setVisibility(View.GONE);//隐藏中间绿色背景的首字符    }};
联系人点击事件当单击联系人时跳转到聊天界面,当长按联系人时弹出Dialog询问用户是否删除好友。  
private ContactListAdapter.OnItemClickListener mOnItemClickListener = new ContactListAdapter.OnItemClickListener() {    /**     * 单击跳转到聊天界面     * @param name 点击item的联系人名字     */    @Override    public void onItemClick(String name) {        startActivity(ChatActivity.class, Constant.Extra.USER_NAME, name);    }    /**     * 长按删除好友     * @param name 点击item的联系人名字     */    @Override    public void onItemLongClick(final String name) {        showDeleteFriendDialog(name);    }};
添加好友界面 

022.png

 MVP实现  
  • AddFriendView
    • AddFriendPresenter
搜索用户 
@Overridepublic void searchFriend(final String keyword) {    mAddFriendView.onStartSearch();    //注:模糊查询只对付费用户开放,付费后可直接使用。    BmobQuery query = new BmobQuery();    query.addWhereContains("username", keyword).addWhereNotEqualTo("username", EMClient.getInstance().getCurrentUser());    query.findObjects(new FindListener() {        @Override        public void done(List list, BmobException e) {            processResult(list, e);        }    });}
 greenDAOgreenDAO是Android SQLite数据库ORM框架的一种。ORM即对象关系映射, object/relational mapping, 将Java对象映射成数据库的表。我们需要存储联系人数据到数据库,在添加好友界面,需要判断搜索出来的用户是否已经是联系人,如果已经是好友,则应显示“已添加”。其他ORM框架 参考 创建实体类    
@Entitypublic class Contact {    @Id    public Long id;    public String userName;}
 初始化    
public void init(Context context) {    DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(context, Constant.Database.DATABASE_NAME, null);    SQLiteDatabase writableDatabase = devOpenHelper.getWritableDatabase();    DaoMaster daoMaster = new DaoMaster(writableDatabase);    mDaoSession = daoMaster.newSession();}
保存联系人
public void saveContact(String userName) {    Contact contact = new Contact();    contact.setUsername(userName);    mDaoSession.getContactDao().save(contact);}
查询联系人    
public List queryAllContacts() {    List list = mDaoSession.getContactDao().queryBuilder().list();    ArrayList contacts = new ArrayList();    for (int i = 0; i < list.size(); i++) {        String contact = list.get(i).getUsername();        contacts.add(contact);    }    return contacts;}
 删除所有联系人
public void deleteAllContacts() {    ContactDao contactDao = mDaoSession.getContactDao();    contactDao.deleteAll();}
发送好友请求 AddFriendItemView里面处理添加好友的点击事件    
@OnClick(R.id.add)public void onClick() {    String friendName = mUserName.getText().toString().trim();    String addFriendReason = getContext().getString(R.string.add_friend_reason);    AddFriendEvent event = new AddFriendEvent(friendName, addFriendReason);//通过EventBus发布添加好友事件    EventBus.getDefault().post(event);}
AddFriendPresenterImpl实现发送好友请求   
@Subscribe(threadMode = ThreadMode.BACKGROUND)public void addFriend(AddFriendEvent event) {    try {        EMClient.getInstance().contactManager().addContact(event.getFriendName(), event.getReason());        ThreadUtils.runOnUiThread(new Runnable() {            @Override            public void run() {                mAddFriendView.onAddFriendSuccess();            }        });    } catch (HyphenateException e) {        e.printStackTrace();        ThreadUtils.runOnUiThread(new Runnable() {            @Override            public void run() {                mAddFriendView.onAddFriendFailed();            }        });    }
在联系人列表中监听联系人变化当新增好友或者被好友删除时刷新联系人列表。 
private EMContactListenerAdapter mEMContactListener = new EMContactListenerAdapter() {    @Override    public void onContactAdded(String s) {        mContactPresenter.refreshContactList();    }    @Override    public void onContactDeleted(String s) {        mContactPresenter.refreshContactList();    }};
聊天界面 

023.png

MVP实现 
  •  ChatView
    •  ChatPresenter
UI监听发送按钮的状态变化     
mEdit.addTextChangedListener(mTextWatcher);private TextWatcherAdapter mTextWatcher = new TextWatcherAdapter() {    @Override    public void afterTextChanged(Editable s) {        mSend.setEnabled(s.length() != 0);    }};
动画文件  
  •  anim文件夹:存放补间动画
    •  animator文件夹:存放属性动画
      •  drawable文件夹:存放帧动画
发送消息的进度动画    
<?xml version="1.0" encoding="utf-8"?>
9文件制作官方说明

24.png


25.png

发送一条消息 返回消息的类型 根据是发送的消息还是接受的消息,来创建不同的item。
@Overridepublic int getItemViewType(int position) {    EMMessage message = mMessages.get(position);    return message.direct() == EMMessage.Direct.SEND ? ITEM_TYPE_SEND_MESSAGE : ITEM_TYPE_RECEIVE_MESSAGE;}
是否显示时间戳的判断 
/** * 如果两个消息之间的时间太近,就不显示时间戳 */private boolean shouldShowTimeStamp(int position) {    long currentItemTimestamp = mMessages.get(position).getMsgTime();    long preItemTimestamp = mMessages.get(position - 1).getMsgTime();    boolean closeEnough = DateUtils.isCloseEnough(currentItemTimestamp, preItemTimestamp);    return !closeEnough;}
更新消息的状态处理三种消息状态,正在发送中INPROGRESS,发送成功SUCCESS,发送失败FAIL。  
private void updateSendingStatus(EMMessage emMessage) {    switch (emMessage.status()) {        case INPROGRESS:            mSendMessageProgress.setVisibility(VISIBLE);            mSendMessageProgress.setImageResource(R.drawable.send_message_progress);            AnimationDrawable drawable = (AnimationDrawable) mSendMessageProgress.getDrawable();            drawable.start();            break;        case SUCCESS:            mSendMessageProgress.setVisibility(GONE);            break;        case FAIL:            mSendMessageProgress.setImageResource(R.mipmap.msg_error);            break;    }}
 接收一条消息 监听消息的接收,当收到一条消息后,通知MessageListAdapter进行刷新,并且滚动RecyclerView到底部。
private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {    @Override    public void onMessageReceived(final List list) {        ThreadUtils.runOnUiThread(new Runnable() {            @Override            public void run() {                final EMMessage emMessage = list.get(0);                mChatPresenter.makeMessageRead(mUserName);                mMessageListAdapter.addNewMessage(emMessage);                smoothScrollToBottom();            }        });    }};
初始化聊天记录    
@Overridepublic void loadMessages(final String userName) {    ThreadUtils.runOnBackgroundThread(new Runnable() {        @Override        public void run() {            EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);            if (conversation != null) {                //获取此会话的所有消息                List messages = conversation.getAllMessages();                mEMMessageList.addAll(messages);                //指定会话消息未读数清零                conversation.markAllMessagesAsRead();            }            ThreadUtils.runOnUiThread(new Runnable() {                @Override                public void run() {                    mChatView.onMessagesLoaded();                }            });        }    });}
加载更多聊天记录   
@Overridepublic void loadMoreMessages(final String userName) {    if (hasMoreData) {        ThreadUtils.runOnBackgroundThread(new Runnable() {            @Override            public void run() {                EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);                EMMessage firstMessage = mEMMessageList.get(0);                //SDK初始化加载的聊天记录为20条,到顶时需要去DB里获取更多                //获取startMsgId之前的pagesize条消息,此方法获取的messages SDK会自动存入到此会话中,APP中无需再次把获取到的messages添加到会话中                final List messages = conversation.loadMoreMsgFromDB(firstMessage.getMsgId(), DEFAULT_PAGE_SIZE);                hasMoreData = (messages.size() == DEFAULT_PAGE_SIZE);                mEMMessageList.addAll(0, messages);                ThreadUtils.runOnUiThread(new Runnable() {                    @Override                    public void run() {                        mChatView.onMoreMessagesLoaded(messages.size());                    }                });            }        });    } else {        mChatView.onNoMoreData();    }}
会话界面

26.png

 MVP实现  
  •  ConversationView
    •  ConversationPresenter



加载所有会话
@Override
public void loadAllConversations() {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
Map conversations = EMClient.getInstance().chatManager().getAllConversations();
mEMConversations.addAll(conversations.values());
//根据最后一条消息的时间进行排序
Collections.sort(mEMConversations, new Comparator() {
@Override
public int compare(EMConversation o1, EMConversation o2) {
return (int) (o2.getLastMessage().getMsgTime() - o1.getLastMessage().getMsgTime());
}
});
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mConversationView.onAllConversationsLoaded();
}
});
}
});
}

未读消息计数更新 
环信官方文档
会话列表中未读消息的更新 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
toast(getString(R.string.receive_new_message));
//当收到一个新消息时,重新加载会话列表。
mConversationPresenter.loadAllConversations();
}
});
}
};

BottomBar badge的更新 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

//该回调在子线程中调用
@Override
public void onMessageReceived(List list) {
updateUnreadCount();
}
};

private void updateUnreadCount() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
BottomBarTab bottomBar = mBottomBar.getTabWithId(R.id.conversations);
int count = EMClient.getInstance().chatManager().getUnreadMsgsCount();
bottomBar.setBadgeCount(count);
}
});
}

标记消息已读 
加载聊天数据时标记已读 
//指定会话消息未读数清零
conversation.markAllMessagesAsRead();

在聊天界面收到新消息时将消息标记已读
   
private EMMessageListenerAdapter mEMMessageListener = new EMMessageListenerAdapter() {
@Override
public void onMessageReceived(final List list) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
final EMMessage emMessage = list.get(0);
if (emMessage.getUserName().equals(mUserName)) {
//标记消息已读
mChatPresenter.makeMessageRead(mUserName);
mMessageListAdapter.addNewMessage(emMessage);
smoothScrollToBottom();
}
}
});
}
};
 聊天界面返回时更新未读badge 
    
@Override
protected void onResume() {
super.onResume();
updateUnreadCount();
}

消息通知
通知 
判断app是否在前台 

 
public boolean isForeground() {
ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
List runningAppProcesses = am.getRunningAppProcesses();
if (runningAppProcesses == null) {
return false;
}
for (ActivityManager.RunningAppProcessInfo info :runningAppProcesses) {
if (info.processName.equals(getPackageName()) && info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
return true;
}
}
return false;
}

 如果app在后台则弹出Notification 
private void showNotification(EMMessage emMessage) {
String contentText = "";
if (emMessage.getBody() instanceof EMTextMessageBody) {
contentText = ((EMTextMessageBody) emMessage.getBody()).getMessage();
}

Intent chat = new Intent(this, ChatActivity.class);
chat.putExtra(Constant.Extra.USER_NAME, emMessage.getUserName());
PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, chat, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new Notification.Builder(this)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.avatar1))
.setSmallIcon(R.mipmap.ic_contact_selected_2)
.setContentTitle(getString(R.string.receive_new_message))
.setContentText(contentText)
.setPriority(Notification.PRIORITY_MAX)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
notificationManager.notify(1, notification);
}
 声音  

 初始化SoundPool
private void initSoundPool() {
mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
mDuanSound = mSoundPool.load(this, R.raw.duan, 1);
mYuluSound = mSoundPool.load(this, R.raw.yulu, 1);
}

 播放音效 
  
private EMMessageListenerAdapter mEMMessageListenerAdapter = new EMMessageListenerAdapter() {

@Override
public void onMessageReceived(List list) {
if (isForeground()) {
mSoundPool.play(mDuanSound, 1, 1, 0, 0, 1);
} else {
mSoundPool.play(mYuluSound, 1, 1, 0, 0, 1);
showNotification(list.get(0));
}
}
};
 多设备登录 
环信官方文档
private EMConnectionListener mEMConnectionListener = new EMConnectionListener() {
@Override
public void onConnected() {

}

@Override
public void onDisconnected(int i) {
if (i == EMError.USER_LOGIN_ANOTHER_DEVICE) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
startActivity(LoginActivity.class);
toast(getString(R.string.user_login_another_device));
}
});
}
}
};

github地址https://github.com/uncleleonfan/QQDemo

1 个评论

iOS版的什么时候提供一个demo呢?

要回复文章请先登录注册