网易云信&杭电课题实践,制作一个在线自习室app
在创新实践课上,老师给了我们一个网易云信与杭电合作的课题实践项目,我们选择了利用网易云信sdk制作一个在线自习APP。第一次做这种使用第三方sdk的项目,也是我的第一次制作安卓app。我负责的是客户端的制作,分享一下我的制作心得。
配套服务端传送门
https://blog.csdn.net/colorglass/article/details/117908845
不得不提网易云信的SDK是非常好用的,比如用户关系托管,加好友等,还有视频通话功能,头像显示功能,特别是UI库,能为我们省下很多时间,样式也比较简洁好看。不过在使用UI库的时候遇到了一些问题,一开始导入一直报错,本来想放弃,但是它真的非常方便,因为时间紧迫,最后还是向它妥协,最后在导师的帮助下解决了问题,因为导入的包有问题,在UI库的帮助下,消息界面的实现会更加方便快捷。其中他的消息收发界面是非常方便的,只需要自己配置头像点击事件即可,其中在配置标题点击事件的时候遇到了点问题,因为使用的是我的模块的页面导致了双向依赖,后来在指导下使用隐式跳转activity解决了这个问题因为在线自习室没有用到群聊功能,session helper代码如下
public class SessionHelper {
private String id;
public static void init(String id) {
// 设置会话中点击事件响应处理
setSessionListener();
// registerViewHolders();
// 注册消息转发过滤器
registerMsgForwardFilter();
// 注册消息撤回过滤器
registerMsgRevokeFilter(id);
// 注册消息撤回监听器
registerMsgRevokeObserver();
}
private static void registerMsgForwardFilter() {
NimUIKit.setMsgForwardFilter(new MsgForwardFilter() {
@Override
public boolean shouldIgnore(IMMessage message) {
if (message.getMsgType() == MsgTypeEnum.robot && message.getAttachment() != null &&
((RobotAttachment) message.getAttachment()).isRobotSend()) {
return true; // 如果是机器人发送的消息 不支持转发
}
return false;
}
});
}
/**
* 消息撤回过滤器
*/
private static void registerMsgRevokeFilter(String id) {
NimUIKit.setMsgRevokeFilter(new MsgRevokeFilter() {
@Override
public boolean shouldIgnore(IMMessage message) {
if (id.equals(message.getSessionId())) {
// 发给我的电脑 不允许撤回
return true;
}
return false;
}
});
}
private static void setSessionListener() {
SessionEventListener listener = new SessionEventListener() {
@Override
public void onAvatarClicked(Context context, IMMessage message) {
// 一般用于打开用户资料页面
PersonalInfo.start(context, message.getFromAccount());
}
@Override
public void onAvatarLongClicked(Context context, IMMessage message) {
// 一般用于群组@功能,或者弹出菜单,做拉黑,加好友等功能
}
@Override
public void onAckMsgClicked(Context context, IMMessage message) {
}
};
NimUIKit.setSessionListener(listener);
}
private static void registerMsgRevokeObserver() {
NIMClient.getService(MsgServiceObserve.class).observeRevokeMessage(new NimMessageRevokeObserver(), true);
}
}
然后是联系人列表,他有个自带的字母排序,他的名字优先级是备注>昵称>账号,同时也需要配置点击事件,它默认是打开聊天,但一般需要展示个人名片
public class ContactHelper {
public static void init() {
setContactEventListener();
}
private static void setContactEventListener() {
NimUIKit.setContactEventListener(new ContactEventListener() {
@Override
public void onItemClick(Context context, String account) {
PersonalInfo.start(context,account);
}
@Override
public void onItemLongClick(Context context, String account) {
}
@Override
public void onAvatarClick(Context context, String account) {
}
});
}
}
这是UI库的消息记录
搜索我参照demo写了两种搜索,一个是消息记录,右上角的图标有个红点提示功能,即有人加你好友会显示红点,有一个需要注意的是一定要在外面注册观察者,否则第一次进入会没有反应,一个是关键字搜索好友,其中搜索消息的锚点配置让我思考了很久,也就是点击消息要能 跳转到这条历史纪录的地方,
这是搜索的效果图,关于显示消息部分代码如下。`protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View rootView = LayoutInflater.from(this).inflate(R.layout.message_history_activity, null);
setContentView(rootView);
ToolBarOptions options = new NimToolBarOptions();
setToolBar(R.id.toolbar, options);
onParseIntent();
Container container = new Container(this, account, sessionType, this);
messageListPanel = new MessageListPanelEx(container, rootView,anchor, true, false);
}
protected void onParseIntent() {
anchor = (IMMessage) getIntent().getSerializableExtra(EXTRA_ANCHOR);
account = anchor.getSessionId();
sessionType = anchor.getSessionType();
setTitle(UserInfoHelper.getUserTitleName(account, sessionType));
}`
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210526132724271.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2VyZWZhcw==,size_16,color_FFFFFF,t_70这是添加好友界面,参照demo写了重复信息的过滤器。同时同意或者拒绝好友对方会收到toast信息
个人信息界面如下,上传头像有一点让人头疼,做了好几天才让人满意,首先是点击的侧边栏,然后是再点击侧边栏头像可以更换。下面是有关上传头像以及更新给云信服务器代码。侧边栏使用的是drawerlayout非常好用
public void showChoosePicDialog(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("设置头像");
String[] items = { "选择本地照片", "拍照" };
builder.setNegativeButton("取消", null);
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent;
switch (which) {
case CHOOSE_PICTURE: // 选择本地照片
intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PICTURE);//启动系统图库
break;
case TAKE_PICTURE: // 拍照
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent,TAKE_PICTURE);//启动摄像头
break;
}
}
});
builder.create().show();
}
private Uri saveBitmap(Bitmap bm) {
tmpDir = new File(Environment.getExternalStorageDirectory() + "/com.app.avater");
if (!tmpDir.exists()) {
tmpDir.mkdir();
}
picture = new File(tmpDir.getAbsolutePath() + "avater.png");
try {
FileOutputStream fos = new FileOutputStream(picture);
bm.compress(Bitmap.CompressFormat.PNG, 85, fos);
fos.flush();
fos.close();
return Uri.fromFile(picture);//返回uri
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private Uri convertUri(Uri uri) {
InputStream is = null;
try {
is = getActivity().getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(is);
is.close();
return saveBitmap(bitmap);//将头像保存到sd卡
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode ==TAKE_PICTURE) {
//摄像头
if (data == null) {
return;//用户点击取消则直接返回
} else {
Bundle extras = data.getExtras();
if (extras != null) {
Bitmap bm = extras.getParcelable("data");
uri_picture = saveBitmap(bm);//将文件保存到sd卡(直接是file类型的bitmap)
startPhotoZoom(uri_picture);
}
}
} else if (requestCode == CHOOSE_PICTURE) {//图库
if (data == null) {
return;
}
uri_picture = data.getData();
Uri fileUri = convertUri(uri_picture);
startPhotoZoom(fileUri);
} else if (requestCode == CROP_SMALL_PICTURE) {//得到图片裁剪后的数据
if (data == null) {//用户点击取消则直接返回
return;
}
Bundle extras = data.getExtras();
if (extras == null) {
return;
}
Bitmap bm = extras.getParcelable("data");
icon = new File(tmpDir.getAbsolutePath() + "avater.png"); //获取最终icon
try {
FileOutputStream fos = new FileOutputStream(icon);
bm.compress(Bitmap.CompressFormat.PNG, 85, fos);
fos.flush();
fos.close();} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
updateAvatar(icon.getPath());
}
}
/**
* 裁剪图片方法实现
*
* @param uri
*/
protected void startPhotoZoom(Uri uri) {
// 调用系统中自带的图片剪裁
finaluri=uri;
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(uri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 300);
intent.putExtra("outputY", 300);
intent.putExtra("return-data", true);
startActivityForResult(intent, CROP_SMALL_PICTURE);
}
uploadAvatarFuture = NIMClient.getService(NosService.class).upload(file, PickImageAction.MIME_JPEG);
uploadAvatarFuture.setCallback(new RequestCallbackWrapper<String>() {
@Override
public void onResult(int code, String url, Throwable exception) {
if (code == ResponseCode.RES_SUCCESS && !TextUtils.isEmpty(url)) {
UserUpdateHelper.update(UserInfoFieldEnum.AVATAR, url, new RequestCallbackWrapper<Void>() {
@Override
public void onResult(int code, Void result, Throwable exception) {
if (code == ResponseCode.RES_SUCCESS) {
ToastHelper.showToast(getContext(), R.string.head_update_success);
onUpdateDone();
} else {
ToastHelper.showToast(getContext(), R.string.head_update_failed);
}
}
}); // 更新资料
} else {
ToastHelper.showToast(getContext(), R.string.user_info_update_failed);
onUpdateDone();
}
}
});
最重要的视频部分如下,
可以通过tag选择自习室标签,自习室有房主,这个列表搜索是用我们自己搭建的服务器数据库实现,点击即可进入自习室,进入前会先进行判断,你若是被踢过,除非房主邀请你,否在你不能再加入这个房间。下拉可以刷新列表,上拉可以加载更多,默认是6个记录,每次加载会多6条,但是每次搜索,换标签,刷新会让它重新变为六条。这三个按钮只有create创建房间有效,另外会在后期更改,是前期测试所用。点击创建会判断房间是否存在,若存在则会提示不进入房间。一般来说所有用户退出,房间会消失。自习室房主有转让权限,踢人的功能,所有人都可以点击他人的小视频与主视频切换,点击头像可查看他人信息,加好友,聊天等功能,房主退出权限会随机转让。邀请功能由信令实现,拒绝,接受,取消等对方都会收到信息,离线邀请会失败,如果他人在房间中也不可以邀请。
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = friends.get(getLayoutPosition());
HttpPostfindisinroom httpPostfindisinroom=new HttpPostfindisinroom(account);
try {
httpPostfindisinroom.start();
if(httpPostfindisinroom.getRoomname().equals("")){
if(ishost){//是房主可以重新赋予权限
HttpPostunKick httpPostunKick=new HttpPostunKick(String.valueOf(roomid),account,myname);
httpPostunKick.start();
showdeldialog(account);
} //不是房主判断是否有权限
else {
HttpPostiskicked httpPostiskicked=new HttpPostiskicked(account,roomid);
httpPostiskicked.start();
if(httpPostiskicked.getData().equals("1")){
ToastHelper.showToast(context, "ta已被踢过,请让房主邀请");
}
else {
showdeldialog(account);
}
}
}
else {
ToastHelper.showToast(context, "对方在房间中");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
```private void showdeldialog(String friendid) {
String requestId = String.valueOf(System.currentTimeMillis());
CallParamBuilder paramBuilder = new CallParamBuilder(ChannelType.VIDEO, friendid, requestId);
paramBuilder.channelExt(roomname);
NIMClient.getService(SignallingService.class).call(paramBuilder).setCallback(
new RequestCallbackWrapper<ChannelFullInfo>() {
@Override
public void onResult(int code, ChannelFullInfo channelFullInfo, Throwable throwable) {
//参考官方文档中关于api以及错误码的说明
if (code == ResponseCode.RES_SUCCESS) {
mychannelFullInfo=channelFullInfo;
ToastHelper.showToast(context, "邀请成功,等待对方接听");
AlertDialog alertDialog=new AlertDialog.Builder(context).setTitle("正在呼叫"+friendid).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
InviteParamBuilder param = new InviteParamBuilder(mychannelFullInfo.getChannelId(), friendid, requestId);
NIMClient.getService(SignallingService.class).cancelInvite(param).setCallback(new RequestCallback<Void>() {
@Override
public void onSuccess(Void param) {
Toast.makeText(context, "取消邀请成功 ", Toast.LENGTH_SHORT).show();
NIMClient.getService(SignallingService.class).leave(mychannelFullInfo.getChannelId(), false, null);
}
@Override
public void onFailed(int code) {
Toast.makeText(context, "取消邀请失败 :code = " + code, Toast.LENGTH_SHORT).show();
}
@Override
public void onException(Throwable exception) {
Toast.makeText(context, "取消邀请异常 :exception = " + exception, Toast.LENGTH_SHORT).show();
}
});
}
}).create();
alertDialog.show();
alertDialog.setCanceledOnTouchOutside(false);
Observer onlineObserver = new Observer<ChannelCommonEvent>() {
@Override
public void onEvent(ChannelCommonEvent event) {
SignallingEventType eventType = event.getEventType();
switch (eventType) {
case REJECT:
ToastHelper.showToast(context, "对方拒绝了邀请");
alertDialog.dismiss();
break;
case ACCEPT:
ToastHelper.showToast(context, "对方接受了邀请");
alertDialog.dismiss();
break;
default:break;
}
NIMClient.getService(SignallingService.class).leave(channelFullInfo.getChannelId(), false, null);
}
};
NIMClient.getService(SignallingServiceObserver.class).observeOnlineNotification(onlineObserver, true);
} else {
if(code==10202){
ToastHelper.showToast(context, "对方离线");
}
}
}
});
``
private void initinvitelisetener() {
// 在线通知事件观察者
Observer onlineObserver = new Observer<ChannelCommonEvent>() {
@Override
public void onEvent(ChannelCommonEvent event) {
SignallingEventType eventType = event.getEventType();
switch (eventType) {
case INVITE:
InvitedEvent invitedEvent = (InvitedEvent) event;
InviteParamBuilder inviteParam = new InviteParamBuilder(event.getChannelBaseInfo().getChannelId(),
event.getFromAccountId(),
invitedEvent.getRequestId());
LayoutInflater layoutInflater=LayoutInflater.from(MainActivity.this);
mydialog=layoutInflater.inflate(R.layout.friend_item,null);
intoroomname=event.getChannelBaseInfo().getChannelExt();
alertDialog=new AlertDialog.Builder(BaseActivity.getCurrentActivity()).setTitle("邀请你加入"+intoroomname).setView(mydialog).setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
HttpPostGetroominfo httpPostGetroominfo=new HttpPostGetroominfo(intoroomname,"0","");
try {
httpPostGetroominfo.start();
if(httpPostGetroominfo.getCode()==1){ //没房间
Toast.makeText(BaseActivity.getCurrentActivity(),"房间不存在",Toast.LENGTH_SHORT).show();
NIMClient.getService(SignallingService.class).rejectInvite(inviteParam);
}
else if(httpPostGetroominfo.getNum()==4){
Toast.makeText(BaseActivity.getCurrentActivity(),"人满啦",Toast.LENGTH_SHORT).show();
NIMClient.getService(SignallingService.class).rejectInvite(inviteParam);
}
else { //进入房间,不是房主,
NIMClient.getService(SignallingService.class).acceptInvite(inviteParam);
roomid=httpPostGetroominfo.getRoomId();
intoroom(myid,roomid,intoroomname);
// intoroom(myid,roomname,false,httpPostGetroominfo.getRoomId());
}
} catch (InterruptedException e) {
e.printStackTrace();}
}
}).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
NIMClient.getService(SignallingService.class).rejectInvite(inviteParam);
}
}).create();
if(alertDialog.isShowing()==false){
HeadImageView headImageView=mydialog.findViewById(R.id.invitehead);
String friendid=event.getFromAccountId();
headImageView.loadBuddyAvatar(friendid);
TextView textView=mydialog.findViewById(R.id.name);
if(NIMClient.getService(FriendService.class).isMyFriend(friendid)){
Friend friend = NIMClient.getService(FriendService.class).getFriendByAccount(friendid);
textView.setText(friend.getAlias());
}
else {
textView.setText(friendid);
}
alertDialog.setCanceledOnTouchOutside(false);
invitedEvent.getChannelBaseInfo().getChannelExt();
alertDialog.show();
}
break;
case CANCEL_INVITE:
if(alertDialog.isShowing()){
CanceledInviteEvent canceledInviteEvent = (CanceledInviteEvent) event;
alertDialog.dismiss();
if(NIMClient.getService(FriendService.class).isMyFriend(canceledInviteEvent.getToAccount())){
Friend friend = NIMClient.getService(FriendService.class).getFriendByAccount(canceledInviteEvent.getToAccount());
Toast.makeText(MainActivity.this,friend.getAlias()+"取消了邀请", Toast.LENGTH_LONG).show();
}
else {
Toast.makeText(MainActivity.this,canceledInviteEvent.getFromAccountId()+"取消了邀请", Toast.LENGTH_LONG).show();
}
}
break;
default:break;
}
}
};
//注册
NIMClient.getService(SignallingServiceObserver.class).observeOnlineNotification(onlineObserver, true);
为了让对话框实现全局显示,在BaseActivity中配置了public static Activity getCurrentActivity() {
return mCurrentActivity;
},使其能在所有页面上显示。
无法呼叫离线好友,可主动取消呼叫,在对方应答后(回调)会话框会关闭
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String account = friends.get(getLayoutPosition());
HttpPostfindisinroom httpPostfindisinroom=new HttpPostfindisinroom(account);
try {
httpPostfindisinroom.start();
if(httpPostfindisinroom.getRoomname().equals("")){
if(ishost){//是房主可以重新赋予权限
HttpPostunKick httpPostunKick=new HttpPostunKick(String.valueOf(roomid),account,myname);
httpPostunKick.start();
showdeldialog(account);
} //不是房主判断是否有权限
else {
HttpPostiskicked httpPostiskicked=new HttpPostiskicked(account,roomid);
httpPostiskicked.start();
if(httpPostiskicked.getData().equals("1")){
ToastHelper.showToast(context, "ta已被踢过,请让房主邀请");
}
else {
showdeldialog(account);
}
}
}
else {
ToastHelper.showToast(context, "对方在房间中");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}); private void showdeldialog(String friendid) {
String requestId = String.valueOf(System.currentTimeMillis());
CallParamBuilder paramBuilder = new CallParamBuilder(ChannelType.VIDEO, friendid, requestId);
paramBuilder.channelExt(roomname);
NIMClient.getService(SignallingService.class).call(paramBuilder).setCallback(
new RequestCallbackWrapper<ChannelFullInfo>() {
@Override
public void onResult(int code, ChannelFullInfo channelFullInfo, Throwable throwable) {
//参考官方文档中关于api以及错误码的说明
if (code == ResponseCode.RES_SUCCESS) {
mychannelFullInfo=channelFullInfo;
ToastHelper.showToast(context, "邀请成功,等待对方接听");
AlertDialog alertDialog=new AlertDialog.Builder(context).setTitle("正在呼叫"+friendid).setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
InviteParamBuilder param = new InviteParamBuilder(mychannelFullInfo.getChannelId(), friendid, requestId);
NIMClient.getService(SignallingService.class).cancelInvite(param).setCallback(new RequestCallback<Void>() {
@Override
public void onSuccess(Void param) {
Toast.makeText(context, "取消邀请成功 ", Toast.LENGTH_SHORT).show();
NIMClient.getService(SignallingService.class).leave(mychannelFullInfo.getChannelId(), false, null);
}
@Override
public void onFailed(int code) {
Toast.makeText(context, "取消邀请失败 :code = " + code, Toast.LENGTH_SHORT).show();
}
@Override
public void onException(Throwable exception) {
Toast.makeText(context, "取消邀请异常 :exception = " + exception, Toast.LENGTH_SHORT).show();
}
});
}
}).create();
alertDialog.show();
alertDialog.setCanceledOnTouchOutside(false);
Observer onlineObserver = new Observer<ChannelCommonEvent>() {
@Override
public void onEvent(ChannelCommonEvent event) {
SignallingEventType eventType = event.getEventType();
switch (eventType) {
case REJECT:
ToastHelper.showToast(context, "对方拒绝了邀请");
alertDialog.dismiss();
break;
case ACCEPT:
ToastHelper.showToast(context, "对方接受了邀请");
alertDialog.dismiss();
break;
default:break;
}
NIMClient.getService(SignallingService.class).leave(channelFullInfo.getChannelId(), false, null);
}
};
NIMClient.getService(SignallingServiceObserver.class).observeOnlineNotification(onlineObserver, true);
} else {
if(code==10202){
ToastHelper.showToast(context, "对方离线");
}
}
}
});
视频如下,如果你是房主则会有三个按钮,不是则只有一个,头像点击可以进入个人页面,第二个是踢人,第三个是转让房主权限。默认进入房间是大的视频框,点击小的可以与大的切换视频对象。