实现 1 对 1 消息
更新时间: 2024/08/14 11:41:36
本文介绍通过消息聊天组件 chatKit 实现 1 对 1 消息。消息聊天组件 chatKit 底层基于IM UIKit。
功能介绍
1 对 1 消息的主要功能包括会话列表、聊天消息、通知消息和语音输入。
前提条件
请确保您已完成以下操作:
准备工作
注册云信 IM 测试账号,获取 accid 和 token
为了方便您调试,您可以在云信控制台注册云信 IM 测试账号,获取 accid 和 token。accid 和 token 将用于登录云信服务端。
-
在云信控制台的首页单击指定应用名称,进入该应用的详情页面。
-
在左侧导航栏选择产品功能 > IM即时通讯,单击基础功能页签。
-
在测试账号管理区域单击子功能配置。
-
在测试账号管理页面,单击新建账号,并填写账号(即accid)、昵称(即 name)、密码(即 Token)后,单击确定。
配置高德地图参数(地理位置消息功能需要)
地理位置消息功能基于高德地图,因此需要您配置高德地图相关信息。请在您应用的AndroidManifest.xml
中配置高德地图的 API Key 和定位服务(APSService)。
xml<!-- 高德地图定位 -->
//在您的applicaion节点中配置高德地图 API Key 和 APSService
<application android:name=".IMApplication">
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="apikey" /> // 传入您获取到的高德地图 API Key
<service android:name="com.amap.api.location.APSService" />
</application>
开发环境
开发环境要求如下:
环境要求 | 说明 |
---|---|
JDK 版本 | JDK 11 及以上版本 |
Android API 版本 | API 21、Android Studio 5.0 及以上版本 |
Gradle 及所需的依赖库 | 在 Gradle Services 页面下载对应版本的 Gradle 及所需的依赖库。
|
CPU架构 | ARM 64、ARMV7 |
IDE | Android Studio |
其他 | 依赖 Androidx,不支持 support 库。 |
示例项目源码
1 对 1 娱乐社交示例项目源码,跑通示例项目的方法请参见跑通示例项目。
步骤1:集成 chatKit 组件
-
在您的项目的
build.gradle
中,以添加依赖的形式添加相应的 chatKit 组件和第三方库(Glide、Retrofit 和 OkHttp)。groovy
dependencies { // 会话列表功能组件 implementation("com.netease.yunxin.kit.conversation:conversationkit-ui:${LATEST_VERSION}") // 聊天功能组件 implementation("com.netease.yunxin.kit.chat:chatkit-ui:${LATEST_VERSION}") //图片库 implementation("com.netease.yunxin.kit.common:common-image:1.1.6") //网络库 implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.retrofit2:converter-scalars:2.9.0") implementation("com.squareup.okhttp3:okhttp:4.9.3") //地理位置消息 implementation("com.netease.yunxin.kit.locationkit:locationkit:${LATEST_VERSION}") }
如果是 kotlin 脚本配置,在应用根目录下,打开
build.gradle.kts
,声明云信 IM UIKit 代码仓库。java
dependencies { api("com.netease.yunxin.kit.contact:contactkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.qchat:qchatkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.conversation:conversationkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.team:teamkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.chat:chatkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.search:searchkit-ui:${LATEST_VERSION}") api("com.netease.yunxin.kit.locationkit:locationkit:${LATEST_VERSION}") }
-
在项目根目录下,打开
gradle.propertes
, 修改资源依赖配置。java
android.nonTransitiveRClass=false
如果导入之后,出现资源找不到的问题,请修改此处配置。
IM UIKit 模块化设计,所以存在资源依赖,以减少包体积。 -
配置防止代码混淆
代码混淆是指使用简短无意义的名称重命名已存在的类、方法、属性等,增加逆向工程的难度,保障 Android 程序源码的安全性。
为了避免因上述的重命名而导致调用 chatKit 异常,请在
proguard-rules.pro
文件中加入以下代码,将 NIM SDK 和 IM UIKit 的相关类加入不混淆名单。java
## NIM SDK 防混淆 -dontwarn com.netease.nim.** -keep class com.netease.nim.** {*;} -dontwarn com.netease.nimlib.** -keep class com.netease.nimlib.** {*;} -dontwarn com.netease.share.** -keep class com.netease.share.** {*;} -dontwarn com.netease.mobsec.** -keep class com.netease.mobsec.** {*;} ## 如果你使用全文检索插件,需要加入 -dontwarn org.apache.lucene.** -keep class org.apache.lucene.** {*;} ## IM UIKit 防混淆 -dontwarn com.netease.yunxin.kit.** -keep class com.netease.yunxin.kit.** {*;} -keep public class * extends com.netease.yunxin.kit.corekit.XKitInitOptions -keep class * implements com.netease.yunxin.kit.corekit.XKitService {*;} ## 呼叫组件防混淆 -keep class com.netease.lava.** {*;} -keep class com.netease.yunxin.** {*;} -dontwarn com.netease.yunxin.kit.** -keep class com.netease.yunxin.kit.** {*;} -keep public class * extends com.netease.yunxin.kit.corekit.XKitInitOptions -keep class * implements com.netease.yunxin.kit.corekit.XKitService {*;} ## glide 4 -keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * extends com.bumptech.glide.module.AppGlideModule -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { **[] $VALUES; public *; } ## okhttp -dontwarn okhttp3.** -keep class okhttp3.**{*;} ## 如果你使用全文检索插件,需要加入 -dontwarn org.apache.lucene.** -keep class org.apache.lucene.** {*;} ## 如果你开启数据库功能,需要加入 -keep class net.sqlcipher.** {*;}
步骤2:初始化 chatKit
SDKOptions options = NimSDKOptionConfig.getSDKOptions(this, “your app key”);
LoginInfo info = new LoginInfo("account","token"); // account 和 token 请替换为 IM 的 accid 和 Token
IMKitClient.init(this, info, options);
if (IMKitUtils.isMainProcess(this)) {
ChatKitClient.init(this);
//初始化位置消息模块
LocationKitClient.init();
}
步骤3:登录IM
如果登录信息(LoginInfo
)在初始化的时候已传入,则不需要再进行本节内容介绍的登录步骤。
如果不能在 Applicatiton 中获取登录信息LoginInfo
,需调用IMKitClient
类中的loginIM
方法登录。
调用登录的方法时,将如下示例代码中的 accid
和 token
分别替换为您的云信账号 ID (即 accid)和 Token。
javaLoginInfo loginInfo = LoginInfo.LoginInfoBuilder.loginInfoDefault("accid","token").build();
IMKitClient.loginIM(loginInfo,new LoginCallback<LoginInfo>() {
@Override
public void onError(int errorCode, @NonNull String errorMsg) {
//登录失败
}
@Override
public void onSuccess(@Nullable LoginInfo data) {
//登录成功
}
});
步骤4:界面搭建
1 对 1 娱乐社交消息系统常用的功能包括会话列表和聊天界面,本文介绍 chatKit 如何基于 IM UIKit 搭建相应界面。
搭建会话列表
基于 Fragment 方式集成 IM UIKit 的会话列表,具体步骤请参见IM UIKit 的集成会话列表。
搭建聊天界面
通过会话消息模块(chatkit-ui)搭建聊天界面,实现接收和发送基本的消息类型,包括文本消息、图片消息、语音消息、视频消息、表情和地理位置消息。
IM UIKit 提供的单聊 Fragment 的类名为ChatP2PFragment
。您可以在应用的 Activity 中集成ChatP2PFragment
从而构建您的单聊界面。
本节假设您的应用的 Acitiviy 为MyChatP2PActivity
,进行相应说明。
-
创建您的应用 Activity 的布局文件
my_chat_p2p_activity.xml
。java
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:ignore="MissingDefaultResource"> //用来放置Fragment <FrameLayout android:id="@+id/chat_container" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
-
创建 Activity。本步骤以应用的 Acitivity 采用视图绑定(ViewBinding)模式为例进行说明。
java
public class MyChatP2PActivity extends AppCompatActivity { private MyChatP2PActivityBinding viewBinding; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewBinding = MyChatP2PActivityBinding.inflate(LayoutInflater.from(this)); //设置Activity界面布局,非ViewBinding采用setContentView(R.layout.my_chat_p2p_activity); setContentView(viewBinding.getRoot()); UserInfo userInfo = (UserInfo) getIntent().getSerializableExtra(RouterConstant.CHAT_KRY); if (userInfo == null) { finish(); } FragmentManager fragmentManager = getSupportFragmentManager(); //创建ChatP2PFragment P2PChatFragmentBuilder fragmentBuilder = new P2PChatFragmentBuilder(); ChatP2PFragment chatFragment = fragmentBuilder.build(); Bundle bundle = new Bundle(); bundle.putSerializable(RouterConstant.CHAT_KRY, userInfo); chatFragment.setArguments(bundle); //将ChatP2PFragment加载到Activity中 fragmentManager .beginTransaction() .add(R.id.chat_container, chatFragment) .commitAllowingStateLoss(); } }
-
在
AndroidManfest.xml
中声明 Activity。java
<activity android:name=".activity.MyChatP2PActivity" android:launchMode="singleTop" android:screenOrientation="portrait" android:windowSoftInputMode="stateHidden|adjustResize"> </activity>
-
注册会话界面。
调用
XKitRouter
中的会话注册方法registerRouter
,同时进行相应配置(参考本步骤的示例代码),将 IM UIKit 的默认会话界面替换为您的 Activity 界面。- 方法原型
java
XKitRouter.registerRouter(path,activity);
- 参数说明
参数 类型 说明 path
String Activity 的跳转地址 activity
Class 跳转的目标 Activity - 示例代码
java
XKitRouter.registerRouter(RouterConstant.PATH_CHAT_P2P_PAGE, MyChatP2PActivity.class);
-
使用 IM UIKit 提供的路由器
XKitRouter
进行跳转。java
XKitRouter.withKey(RouterConstant.PATH_CHAT_P2P_PAGE).withContext(context).navigate();
实现通知消息
调用以下方法实现发送通知消息,通知消息只有本端可见,对方不可见。
示例代码如下:
// 注册自定义消息解析
ChatKitClient.addCustomAttach(
OneOnOneChatCustomMessageType.ACCOST_MESSAGE_TYPE, AccostMessageAttachment.class);
ChatKitClient.addCustomAttach(
OneOnOneChatCustomMessageType.TRY_AUDIO_CALL_MESSAGE_TYPE,
TryAudioCallMessageAttachment.class);
ChatKitClient.addCustomAttach(
OneOnOneChatCustomMessageType.TRY_VIDEO_CALL_MESSAGE_TYPE,
TryVideoCallMessageAttachment.class);
// 注册自定义消息UI
ChatKitClient.addCustomViewHolder(
OneOnOneChatCustomMessageType.ACCOST_MESSAGE_TYPE, AccostMessageViewHolder.class);
ChatKitClient.addCustomViewHolder(
OneOnOneChatCustomMessageType.TRY_AUDIO_CALL_MESSAGE_TYPE,
TryAudioCallMessageViewHolder.class);
ChatKitClient.addCustomViewHolder(
OneOnOneChatCustomMessageType.TRY_VIDEO_CALL_MESSAGE_TYPE,
TryVideoCallMessageViewHolder.class);
搭建语音输入页面
示例代码如下:
// 显示语音录制弹窗
private void showAudioInputDialog() {
if (!PermissionUtils.hasPermissions(this, Manifest.permission.RECORD_AUDIO)) {
permissionLauncher.launch(new String[] {Manifest.permission.RECORD_AUDIO});
return;
}
audioInputManager.initAudioRecord(this);
audioInputManager.startAudioRecord();
if (dialog == null) {
dialog = new AudioInputDialog(CustomChatP2PActivity.this, sessionId);
}
if (!dialog.isShowing()) {
dialog.show();
dialog.showCancelAudioSendUI(false);
}
}
// 结束语音录制并隐藏语音录制弹窗
audioInputManager.endAudioRecord(isInsideView);
dismissAudioInputDialog();
// 暂停语音录制
AudioInputManager.getInstance().pause();
// 销毁语音录制
AudioInputManager.getInstance().destroy();
// 监听语音输入按钮触摸事件
mAudioTv.setOnTouchListener(
(v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (OneOnOneUtils.isInVoiceRoom()) {
ToastX.showShortToast(R.string.one_on_one_other_you_are_in_the_chatroom);
} else {
showAudioInputDialog();
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
if (dialog != null) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
isInsideView = dialog.getFlViewRect().contains(x, y);
dialog.showCancelAudioSendUI(isInsideView);
}
} else if (event.getAction() == MotionEvent.ACTION_UP) {
if (dialog != null) {
audioInputManager.endAudioRecord(isInsideView);
dismissAudioInputDialog();
}
}
return true;
});
// 语音输入弹窗UI
AudioInputDialog
// 语音输入逻辑
AudioInputManager
// 初始化语音录制
public void initAudioRecord(Context context) {
ALog.i(TAG, "initAudioRecord,context:" + context);
this.context = context;
if (mAudioRecorder == null) {
mAudioRecorder = new AudioRecorder(context, RecordType.AAC, MAX_DURATION, this);
}
}
// 开始语音录制
public void startAudioRecord() {
ALog.i(TAG, "startAudioRecord");
if (context instanceof Activity) {
((Activity) context)
.getWindow()
.setFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
if (mAudioRecorder != null) {
mAudioRecorder.startRecord();
}
timer = new Timer();
TimerTask timerTask =
new TimerTask() {
@Override
public void run() {
if (!started) {
return;
}
currentMilliSecond = currentMilliSecond + PERIOD;
if (audioInputCallback != null) {
audioInputCallback.onAudioRecordProgress(
(int) (currentMilliSecond * 1.0 / MAX_SECOND_MILLIS * 100));
}
if (currentMilliSecond >= MAX_SECOND_MILLIS) {
endAudioRecord(false);
}
}
};
timer.schedule(timerTask, 0, PERIOD);
}
// 结束语音录制
public void endAudioRecord(boolean cancel) {
ALog.d(TAG, "endAudioRecord -->> cancel:" + cancel);
reset();
mainHandler.post(
() -> {
if (context instanceof Activity) {
((Activity) context)
.getWindow()
.setFlags(0, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
if (mAudioRecorder != null) {
mAudioRecorder.completeRecord(cancel);
}
});
}