您的位置:首页 > 其它

基于云信IM实现的文字+图片消息聊天功能

2018-07-24 20:46 671 查看
版权声明:转载请附上原地址。 https://blog.csdn.net/hq942845204/article/details/81191979

前言

记得在初学Android时,自己当时定下的目标的是实现一个QQ,虽然当时的想法比较高,但是自己当时技术不足,很多功能无从下手,最后便做了一些QQ的效果来当做学习,后来就搁置在那里了,然后在大二暑假在工作室做项目,项目里需要用到一个客服的功能,其实就是一个在线聊天,当时也是花了很多功夫,最后是借助三方平台融云的IM来实现的,不过当时时间很紧,于是没有将过程记录下来,正好最近接触到了云信,于是补上一篇实现聊天的博客,功能实现借助的是三方平台–云信,主要是使用了一下云信的IM功能,在这里也非常感谢云信能提供给开发者的服务,点个赞,同时SDK使用起来真的非常方便友好,不过云信的功能是要付费的哦,今天客服小姐姐给我打电话才知道,/略尴尬。

开发前的准备

在开发前,我们首先要去官网注册一个开发者账号,然后创建一个自己的app,创建app后,就会自动生成app对应的appkey,然后为了方便调试,我们在app的管理界面创建两个调试账号,如图,点击账号管理即可创建

这两个调试账号用于我们聊天过程中的两个账号互相聊天。
也就是一共需要三个东西,一个appkey,二个调试账号。

正文

首先,我们创建好项目后,需要对项目进行配置,配置方法,官方帮助文档已经说的非常详细了,就不赘述了,我就直接贴一下我配置的,因为我只需要用到IM功能,所以我只配置了如下几个依赖

// 添加依赖。注意,版本号必须一致。
// 基础功能 (必需)
implementation 'com.netease.nimlib:basesdk:5.4.0'
// 聊天室需要
implementation 'com.netease.nimlib:chatroom:5.4.0'
// 小米、华为、魅族、fcm 推送
implementation 'com.netease.nimlib:push:5.4.0'

然后再手动导入相应的so包

再在清单文件中注册相应的服务和广播,具体可以去文章末尾下载源码查看,需要说明的是,如果要运行的话,需要将下面的value值更换为你自己申请的key值,否则在最后登陆的时候,会提示登陆失败

<meta-data
android:name="com.netease.nim.appKey"
android:value="380d3252cff90baf6dc1718ff931ae70" />

配置工作准备完毕,我们现在开始编写代码,首先我们新建一个BaseApplication,在里面初始化云信的SDK,代码如下

public class BaseApplication extends Application{

@Override
public void onCreate() {
super.onCreate();

// SDK初始化(启动后台服务,若已经存在用户登录信息, SDK 将完成自动登录)
NIMClient.init(this, loginInfo(), options());

}

// 如果返回值为 null,则全部使用默认参数。
private SDKOptions options() {
SDKOptions options = new SDKOptions();

// 如果将新消息通知提醒托管给 SDK 完成,需要添加以下配置。否则无需设置。
StatusBarNotificationConfig config = new StatusBarNotificationConfig();
config.notificationEntrance = ChatActivity.class; // 点击通知栏跳转到该Activity
config.notificationSmallIconId = R.mipmap.ic_launcher_round;
// 呼吸灯配置
config.ledARGB = Color.GREEN;
config.ledOnMs = 1000;
config.ledOffMs = 1500;
// 通知铃声的uri字符串
config.notificationSound = "android.resource://com.netease.nim.demo/raw/msg";
options.statusBarNotificationConfig = config;

// 配置保存图片,文件,log 等数据的目录
// 如果 options 中没有设置这个值,SDK 会使用下面代码示例中的位置作为 SDK 的数据目录。
// 该目录目前包含 log, file, image, audio, video, thumb 这6个目录。
// 如果第三方 APP 需要缓存清理功能, 清理这个目录下面个子目录的内容即可。
String sdkPath = Environment.getExternalStorageDirectory() + "/" + getPackageName() + "/nim";
options.sdkStorageRootPath = sdkPath;

// 配置是否需要预下载附件缩略图,默认为 true
options.preloadAttach = true;

// 配置附件缩略图的尺寸大小。表示向服务器请求缩略图文件的大小
// 该值一般应根据屏幕尺寸来确定, 默认值为 Screen.width / 2
options.thumbnailSize = 480/2;

// 用户资料提供者, 目前主要用于提供用户资料,用于新消息通知栏中显示消息来源的头像和昵称
options.userInfoProvider = new UserInfoProvider() {
@Override
public UserInfo getUserInfo(String account) {
return null;
}

@Override
public String getDisplayNameForMessageNotifier(String account, String sessionId,
SessionTypeEnum sessionType) {
return null;
}

@Override
public Bitmap getAvatarForMessageNotifier(SessionTypeEnum sessionType, String sessionId) {
return null;
}
};
return options;
}

// 如果已经存在用户登录信息,返回LoginInfo,否则返回null即可
private LoginInfo loginInfo() {
SharedPreferences sp=getSharedPreferences("userinfo",MODE_PRIVATE);
String userStr=sp.getString("userLogin","");
if(!TextUtils.isEmpty(userStr)){
return new Gson().fromJson(userStr, new TypeToken<LoginInfo>(){}.getType());
}
return null;
}

}

首先我的初始化方法是用官方推荐的方法,参数二,使用

loginInfo()
方法获取本地的用户信息,这个就是如果用户登录了,那么就将用户的信息保存到本地,然后下次初始化就不用获取这些信息了,参数三
options()
方法主要是进行一些参数配置,代码中的注释已经很清楚了。
然后我们将AndroidManifest.xml中的Application节点改为自己的BaseApplication。
然后我们首先从登陆开始。写一个简单的布局,2个Edittext和一个Button,然后输入用户名和密码,点击按钮登陆,所以核心代码就是登陆怎么写,看登陆的核心方法:

private void login() {
//封装登录信息.
LoginInfo info = new LoginInfo(et1.getText().toString(), et2.getText().toString());
//请求服务器的回调
RequestCallback<LoginInfo> callback =
new RequestCallback<LoginInfo>() {
@Override
public void onSuccess(LoginInfo param) {
Toast.makeText(MainActivity.this, "登录成功", Toast.LENGTH_SHORT).show();

// 可以在此保存LoginInfo到本地,下次启动APP做自动登录用
SharedPreferences.Editor editor=sp.edit();
editor.putString("userLogin",gson.toJson(param));
//跳转到消息页面
startActivity(new Intent(MainActivity.this, ContactActivity.class));
//NimUIKit.startP2PSession(MainActivity.this, "1234");
finish();
}

@Override
public void onFailed(int code) {
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_SHORT).show();

}

@Override
public void onException(Throwable exception) {
Toast.makeText(MainActivity.this, exception.toString(), Toast.LENGTH_SHORT).show();
}

};
//发送请求.
NIMClient.getService(AuthService.class).login(info)
.setCallback(callback);

}

可以看到,我们首先封装一个

LoginInfo
对象,然后声明一个请求登陆服务的回调,在回调中再根据登陆结果做出相应的动作,其中要注意的就是在登陆成功后记得使用
SharedPreferences
保存用户的数据,最后我们再调用login方法登陆,同时将之前声明的回调设置上去,不过说句题外话,这种链式的操作我是非常喜欢的,简洁明了。
登陆成功之后,第二个界面我设置为选择聊天对象的界面,界面元素也很简单,一个
ReyclerView
的列表,显示几个联系人的信息,然后这里可以设置为之前申请的二个调试账号,然后在点击相应的联系人的时候,将对应的用户信息传递到第三个界面–聊天界面上去,下面重点说一下聊天界面的实现,在开始之前,还是先看一下最后的效果

界面中消息的界面采用RecyclerView,根据消息的情况进行收发,所以所有的核心操作都在RecyclerView的适配器里。
首先为了消息的方便管理,我自己定义了一个消息实体,只是为了简化操作

public class MessageEntity{

private String message;//消息的文字内容
private boolean isMine;//是否为自己发出
private int msgType;//消息类型
private String imagePath;//图片消息中图片的路径

public MessageEntity(String message,String imagePath,int msgType, boolean isMine) {
this.message = message;
this.imagePath=imagePath;
this.msgType=msgType;
this.isMine = isMine;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public int getMsgType() {
return msgType;
}

public void setMsgType(int msgType) {
this.msgType = msgType;
}

public String getImagePath() {
return imagePath;
}

public void setImagePath(String imagePath) {
this.imagePath = imagePath;
}

public boolean isMine() {
return isMine;
}

public void setMine(boolean mine) {
isMine = mine;
}

@Override
public String toString() {
return "MessageEntity{" +
"message='" + message + '\'' +
", isMine=" + isMine +
", msgType=" + msgType +
", imagePath='" + imagePath + '\'' +
'}';
}
}

然后我们再来看看

RecyclerView
的适配器怎么实现,首先根据消息的情况,我将消息分为了两大类:自己发出的消息和收到的消息,这两种消息需要根据情况作出不同的处理,所以我们列表项的布局需要定义二个,一个用于显示自己发出的消息,一个用于显示收到的消息,然后我增加了一个图片发送和接收的功能,这里的处理是:首先在消息列表项里适当的位置放置好显示文字的
TextView
和显示图片的
ImageView
,如果消息类型字段
msgType
为图片类型,那么将
TextView
置空,如果为文字类型,那么将
ImaeView
置为不可见,这样就可以既发文字又发图片了,至于语音和地图之类的,这个只需要将
msgType
字段多设置几个类型即可,然后修改对应的列表项布局再去扩展。
好了,有了上面的思路,我们再看看适配器的代码

public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {

private ArrayList<MessageEntity> mData;
private OnItemClickListener onItemClickListener;
private LayoutInflater inflater;
private Context context;

public enum ITEM_TYPE {
ITEM_TYPE_MINE,
ITEM_TYPE_OTHER
}

public MessageAdapter(Context context,ArrayList<MessageEntity> data) {
this.context=context;
this.mData = data;
inflater=LayoutInflater.from(context);
}

public void updateData(ArrayList<MessageEntity> data) {
this.mData = data;
notifyDataSetChanged();
}

//参数二为itemView的类型,viewType代表这个类型值
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
View v;
if(viewType==ITEM_TYPE.ITEM_TYPE_MINE.ordinal()){
v= inflater.inflate(R.layout.item_cv_mine, viewGroup, false);
}else{
v= inflater.inflate(R.layout.item_cv_other, viewGroup, false);
}
return new ViewHolder(v);
}

@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
// 绑定数据
MessageEntity entity=mData.get(position);
if(entity.getMsgType()==1){
holder.mTv.setVisibility(View.VISIBLE);
holder.mTv.setText(entity.getMessage());
holder.mIv.setVisibility(View.GONE);
}else{
holder.mIv.setVisibility(View.VISIBLE);
holder.mTv.setVisibility(View.GONE);
RequestOptions options = new RequestOptions()
.transforms(new RotateTransformation(ImageUtils.parseImageDegree(entity.getImagePath())));
Glide.with(context).load(entity.getImagePath()).apply(options).into(holder.mIv);
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemClick(holder.itemView, pos);
}
}
});

holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (onItemClickListener != null) {
int pos = holder.getLayoutPosition();
onItemClickListener.onItemLongClick(holder.itemView, pos);
}
//表示此事件已经消费,不会触发单击事件
return true;
}
});
}

@Override
public int getItemViewType(int position) {
if(mData.get(position).isMine()){
return ITEM_TYPE.ITEM_TYPE_MINE.ordinal();
}
return ITEM_TYPE.ITEM_TYPE_OTHER.ordinal();
}

@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}

public static class ViewHolder extends RecyclerView.ViewHolder {

TextView mTv;
ImageView mIv;

public ViewHolder(View itemView) {
super(itemView);
mTv = itemView.findViewById(R.id.item_tv);
mIv=itemView.findViewById(R.id.iv);
}
}

public void addNewItem(MessageEntity entity) {
if (mData == null) {
mData = new ArrayList<>();
}
mData.add(getItemCount(), entity);
notifyDataSetChanged();
}

public void deleteItem(int position) {
if (mData == null || mData.isEmpty()) {
return;
}
mData.remove(position);
notifyDataSetChanged();
}

public void setOnItemClickListener(MessageAdapter.OnItemClickListener listener) {
this.onItemClickListener = listener;
}

public interface OnItemClickListener {
void onItemClick(View view, int position);

void onItemLongClick(View view, int position);
}

}

ITEM_TYPE
枚举类型就是代表当前消息的来源,一个是自己发出的,一个是接收到的,在
onCreateViewHolder
方法中根据viewType参数来设置对应的布局,然后我们在
onBindViewHolder
方法中根究
viewType
字段的值去判断是图片消息还是文字消息,再作出相应的逻辑处理,当然不要忘记重写
getItemViewType
方法来设置列表项类型,不然我们在
onCreateViewHolder
中根据参数
viewType
是获取不到的。
有了适配器之后,我们再在Activity中作出相应的逻辑操作,首先是发送消息,代码如下

//发送消息
// 以单聊类型为例
SessionTypeEnum sessionType = SessionTypeEnum.P2P;
String text = et3.getText().toString();
// 创建一个文本消息
IMMessage textMessage = MessageBuilder.createTextMessage(account, sessionType, text);
// 发送给对方
NIMClient.getService(MsgService.class).sendMessage(textMessage, false);
//tv2.setText(text);
mAdapter.addNewItem(new MessageEntity(text,null,1,true));
mRecyclerView.scrollToPosition(list.size());
et3.setText("");

首先获取当前聊天的类型,单聊还是群聊等,这里设置为单聊,然后利用

MessageBuilder
构建一个
IMMessage
对象,其中accout代表账号,最后再调用
NIMClient
getService
方法获取服务,然后调用
sendMessage
来发送
IMMessage
对象, 整个过程还是很简单的,发送完成之后,我们还要更新我们的界面,调用adapter提供的
addNewItem
方法,构建一个
MessageEntity
对象,因为我这里例子是发送文本消息,所以参数一为文本内容,参数二图片路径为空,参数三消息类型为1(1代表文本消息,2代表图片消息),参数四代表是否为本人发出,这里是我们主动发出的消息,所以设置为true。然后为了用户体验效果更好,在每发送一条消息之后,将
RecyclerView
滚动到当前消息列表的最下方,最后再将
EditText
置空,完毕。
上面的是发送文字消息,接下来看怎么实现发送图片消息。
首先发送消息,我设置的为先跳转到媒体的图片选择界面,获取到对应的图片之后,再将图片以消息的形式发送出去。
图片按钮点击事件如下:

Intent intent = new Intent(Intent.ACTION_PICK);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");
startActivityForResult(intent,1);

在onActivityResult方法中的代码如下:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if(requestCode==1){
//获取真实路径,防止在某些机型,如小米中,获取的路径为空
Uri uri=Uri.parse(PathUtils.getRealUri(ChatActivity.this,data.getData()));
//转化为file文件
File imageFile=new File(uri.toString());
//构造图片消息对象
IMMessage message = MessageBuilder.createImageMessage(account,SessionTypeEnum.P2P, imageFile, imageFile.getName());
//发送图片消息
NIMClient.getService(MsgService.class).sendMessage(message, false);
mAdapter.addNewItem(new MessageEntity(null,data.getData().toString(),2,true));
mRecyclerView.scrollToPosition(list.size());
}
}

好了,我们现在已经可以发送消息了,接下来就是消息的接收,消息的接收按照官方推荐,采用观察者模式,在

onCreate
方法中注册,同时要根据收到的消息类型作一下判断,看收到的消息是文本消息还是图片消息,然后再更新
adapter
的数据。代码如下:

private void initMessageObserver(){
// 处理新收到的消息,为了上传处理方便,SDK 保证参数 messages 全部来自同一个聊天对象。
//消息接收观察者
incomingMessageObserver = new Observer<List<IMMessage>>() {
@Override
public void onEvent(List<IMMessage> messages) {
// 处理新收到的消息,为了上传处理方便,SDK 保证参数 messages 全部来自同一个聊天对象。
IMMessage imMessage = messages.get(0);

if(imMessage.getMsgType().equals(MsgTypeEnum.text)){//文本消息
String messageStr=imMessage.getContent();
mAdapter.addNewItem(new MessageEntity(messageStr,null,1,false));
}else if(imMessage.getMsgType().equals(MsgTypeEnum.image)){//图片消息
ImageAttachment msgAttachment=(ImageAttachment)imMessage.getAttachment();

String uri=msgAttachment.getThumbUrl();
mAdapter.addNewItem(new MessageEntity(null,uri,2,false));
}

account = imMessage.getFromAccount();
}
};
//注册消息接收观察者,
//true,代表注册.false,代表注销
NIMClient.getService(MsgServiceObserve.class)
.observeReceiveMessage(incomingMessageObserver, true);
}

注意在获取图片消息这里,看官方文档好久没明白,消息的接收(除了文本消息)这里说的有点简略,然后一直纠结于怎么获取

ImageAttachment
对象,后来跑去官方例子demo中看源码才找到,原来只需要强制转换一下就行,然后得到
ImageAttachment
对象后,我们就可以获取路径,然后交给适配器去处理了
最后这里的消息接收观察者,官方推荐在
onDestroy
里注销一下,如下

@Override
protected void onDestroy() {
super.onDestroy();
//注销消息接收观察者.
NIMClient.getService(MsgServiceObserve.class)
.observeReceiveMessage(incomingMessageObserver, false);
}

至此,核心功能基本都实现了,剩下的就是些细枝末节的东西了,比如动态权限的申请,发送图片时部分机型的图片旋转问题,消息的背景图等等,还有互踢下线的功能官方文档也提供了详细的解决方案。
看下最终的图片发送效果
消息的发送方:

消息的接收方如下

然后在实现完单聊功能的时候,云信还默认帮我们实现了通知栏的推送,最后运行的时候,你会发现,如果你当前的应用不在前台的话,就会接收到相关的消息推送,点击推送,即可进入对应的聊天界面,效果如下

结语

好了,本篇告一段落,嘻嘻嘻,项目当中当然肯定还有很多不足的地方,不过有了雏形,后续就各自发挥啦,初次接触云信IM的朋友可以参考参考,同时,云信非常的贴心,还提供了UI组件供开发者直接使用,不过我没考虑使用,因为我想自己动手做,实际开发的话,还是使用云信提供的UI组件比较好,毕竟是人家封装的,功能细节完善程度都是很好的,另外对于本文中的例子有什么疑问的,欢迎留言交流,我基本每天在线。

不知道为啥我对即时通讯好像特别感兴趣,之前写过一个简单的socket通信demo,感觉socket通信入门也不是特别难,准备考虑后期自己搭一个socket通信的服务器,然后将云信IM部分也自己手动来实现,到时候做完了,再整理一篇博客。

最后附上一下后来整理的之前写的QQ简仿的博客,有兴趣的可以去瞅瞅,毕竟这是我学Android时做的第一个小app,到现在都没舍得删,静静的躺在我的手机里,嘿嘿!
安卓开发个人小作品(2)- QQ简仿

源码下载

本博客例子源码下载:
源码下载

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐