Kotlin高仿微信-项目实践58篇详细讲解了各个功能点,包括:注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。
效果图:
详细的聊天功能请查看Kotlin高仿微信-第8篇-单聊,这里是提示文本功能的部分实现。
实现代码:
我的语言布局:
好友的语音布局:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/5/4 17:36 * Description : 聊天语言按钮 */ class ChatRecordButton : AppCompatButton { constructor(context: Context) : super(context) constructor(context: Context, attributeSet: AttributeSet) :super(context, attributeSet) constructor(context: Context, attributeSet: AttributeSet, defStyle : Int) : super(context, attributeSet, defStyle) private var mFile : String = "" private var mFileMp3 : String = "" private var finishedListener: OnFinishedRecordListener? = null /** * 最短录音时间 */ private val MIN_INTERVAL_TIME = 1000 /** * 最长录音时间 */ private val MAX_INTERVAL_TIME = 1000 * 60 private var view: View? = null private var mStateTV: TextView? = null private var mStateIV: ImageView? = null companion object var mRecorder: MediaRecorder? = null private val mThread: ObtainDecibelThread? = null private var volumeHandler: Handler? = null private var y1 : Float = 0f private var anim: AnimationDrawable? = null fun setOnFinishedRecordListener(listener: OnFinishedRecordListener?) { finishedListener = listener } private var startTime: Long = 0 private var recordDialog: Dialog? = null private val res = intArrayOf( R.drawable.wc_chat_volume_0, R.drawable.wc_chat_volume_1, R.drawable.wc_chat_volume_2, R.drawable.wc_chat_volume_3, R.drawable.wc_chat_volume_4, R.drawable.wc_chat_volume_5, R.drawable.wc_chat_volume_6, R.drawable.wc_chat_volume_7, R.drawable.wc_chat_volume_8 ) //private var audioRecordTool: AudioRecordTool? = null init { volumeHandler = object : Handler() { override fun handleMessage(msg: Message) { if (msg.what == -100) { stopRecording() recordDialog!!.dismiss() } else if (msg.what != -1) { mStateIV!!.setImageResource(res.get(msg.what)) } } } } override fun onTouchEvent(event: MotionEvent?): Boolean { val action = event!!.action y1 = event.y if (mStateTV != null && mStateIV != null && y1 < 0) { mStateTV?.text = "松开手指,取消发送" mStateIV?.setImageDrawable(resources.getDrawable(R.drawable.wc_chat_volume_cancel)) } else if (mStateTV != null) { mStateTV?.text = "手指上滑,取消发送" } when (action) { MotionEvent.ACTION_DOWN -> { text = "松开发送" initDialogAndStartRecord() } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { this.text = "按住录音" if (y1 >= 0 && System.currentTimeMillis() - startTime <= MAX_INTERVAL_TIME) { TagUtils.d("结束录音:") finishRecord() } else if (y1 < 0) { //当手指向上滑,会cancel cancelRecord() } } } return true } /** * 初始化录音对话框 并 开始录音 */ private fun initDialogAndStartRecord() { startTime = System.currentTimeMillis() recordDialog = Dialog(context, R.style.like_toast_dialog_style) // view = new ImageView(getContext()); view = inflate(context, R.layout.wc_chat_dialog_record, null) mStateIV = view?.findViewById(R.id.rc_audio_state_image) mStateTV = view?.findViewById (R.id.rc_audio_state_text) mStateIV?.setImageDrawable(resources.getDrawable(R.drawable.anim_mic)) anim = mStateIV?.getDrawable() as AnimationDrawable anim?.start() mStateIV?.setVisibility(VISIBLE) //mStateIV.setImageResource(R.drawable.ic_volume_1); mStateTV?.setVisibility(VISIBLE) mStateTV?.setText("手指上滑,取消发送") recordDialog?.setContentView(view!!, LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT) ) recordDialog?.setOnDismissListener(onDismiss) val lp = recordDialog?.getWindow()!!.attributes lp.gravity = Gravity.CENTER startRecording() recordDialog?.show() } /** * 放开手指,结束录音处理 */ private fun finishRecord() { val intervalTime: Long = System.currentTimeMillis() - startTime if (intervalTime < MIN_INTERVAL_TIME) { TagUtils.d("录音时间太短") volumeHandler!!.sendEmptyMessageDelayed(-100, 500) //view.setBackgroundResource(R.drawable.ic_voice_cancel); mStateIV!!.setImageDrawable(resources.getDrawable(R.drawable.wc_chat_volume_wraning)) mStateTV!!.text = "录音时间太短" anim!!.stop() val file = File(mFile!!) file.delete() return } else { stopRecording() recordDialog!!.dismiss() } TagUtils.d("录音完成的路径:$mFile") val mediaPlayer = MediaPlayer() try { mediaPlayer.setDataSource(mFile) mediaPlayer.prepare() mediaPlayer.duration TagUtils.d("获取到的时长:" + mediaPlayer.duration / 1000) } catch (e: Exception) { } //if (finishedListener != null) finishedListener!!.onFinishedRecord( mFile, mediaPlayer.duration / 1000) //删除原文件 TagUtils.d("amr录音保存路径:${mFile}") File(mFile).delete() /*TagUtils.d("pcm录音保存路径:${audioRecordTool?.pcmFileName}") audioRecordTool?.deletePcmFile() //最终使用的是wav录音文件,音质非常的好 mFile = audioRecordTool?.wavFileName!! TagUtils.d("wav录音保存路径:${audioRecordTool?.wavFileName!!}")*/ TagUtils.d("mp3录音保存路径:${mFileMp3}") if (finishedListener != null) finishedListener!!.onFinishedRecord( mFileMp3, mediaPlayer.duration / 1000) } /** * 取消录音对话框和停止录音 */ fun cancelRecord() { stopRecording() recordDialog!!.dismiss() val file = File(mFile) file.delete() LameMp3Manager.instance.cancelRecorder() var mp3File = File(mFileMp3) mp3File.delete() } /** * 执行录音操作 */ private fun startRecording() { if(ContextCompat.checkSelfPermission(WcApp.getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){ mFile = FileUtils.getBaseFile("voice_" + System.currentTimeMillis() + ".amr").absolutePath mFileMp3 = FileUtils.getBaseFile("voice_" + System.currentTimeMillis() + ".mp3").absolutePath } else { ToastUtils.makeText(R.string.wc_permission_tip_record) return } //使用MP3文件代替wav,因为mp3语音清晰,占用空间小 LameMp3Manager.instance.startRecorder(mFileMp3) /*audioRecordTool = AudioRecordTool() //初始化 audioRecordTool?.createAudioRecord() audioRecordTool?.start()*/ if (mRecorder != null) { mRecorder?.reset() } else { mRecorder = MediaRecorder() } mRecorder?.setAudioSource(MediaRecorder.AudioSource.MIC) mRecorder?.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB) mRecorder?.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) mRecorder?.setOutputFile(mFile) try { mRecorder?.prepare() mRecorder?.start() } catch (e: Exception) { TagUtils.d("preparestart异常,重新开始录音:$e") e.printStackTrace() mRecorder?.release() mRecorder = null startRecording() } } private fun stopRecording() { //audioRecordTool?.stop() LameMp3Manager.instance.stopRecorder() if (mThread != null) { mThread.exit() //mThread = null } if (mRecorder != null) { try { mRecorder?.stop() //停止时没有prepare,就会报stop failed mRecorder?.reset() mRecorder?.release() mRecorder = null } catch (pE: RuntimeException) { pE.printStackTrace() } finally { if (recordDialog!!.isShowing) { recordDialog!!.dismiss() } } } } private class ObtainDecibelThread(val obj: ChatRecordButton) : Thread() { @Volatile private var running = true fun exit() { running = false } override fun run() { TagUtils.d("检测到的分贝001:") while (running) { if (obj.mRecorder == null || !running) { break } // int x = recorder.getMaxAmplitude(); //振幅 val db: Int = obj.mRecorder!!.getMaxAmplitude() / 600 TagUtils.d("检测到的分贝002:$") if (db != 0 && obj.y1 >= 0) { val f = (db / 5) if (f == 0) obj.volumeHandler?.sendEmptyMessage(0) else if (f == 1) obj.volumeHandler?.sendEmptyMessage( 1 ) else if (f == 2) obj.volumeHandler?.sendEmptyMessage(2) else if (f == 3) obj.volumeHandler?.sendEmptyMessage( 3 ) else if (f == 4) obj.volumeHandler?.sendEmptyMessage(4) else if (f == 5) obj.volumeHandler?.sendEmptyMessage( 5 ) else if (f == 6) obj.volumeHandler?.sendEmptyMessage(6) else obj.volumeHandler?.sendEmptyMessage( 7 ) } obj.volumeHandler?.sendEmptyMessage(-1) if (System.currentTimeMillis() - obj.startTime > 20000) { obj.finishRecord() } try { sleep(200) } catch (e: InterruptedException) { e.printStackTrace() } } } } private val onDismiss = DialogInterface.OnDismissListener { stopRecording() } interface OnFinishedRecordListener{ fun onFinishedRecord(audioPath : String, time : Int) } }
//录音完成回调 chat_record_btn.setOnFinishedRecordListener(object : ChatRecordButton.OnFinishedRecordListener{ override fun onFinishedRecord(audioPath: String, time: Int) { chat_content.isFocusable = false SoftInputUtils.hideSoftInput(chat_record_btn) AddFileListener.sendFile(ChatBean.CONTENT_TYPE_VOICE, audioPath, toUserId, time) } })
/** * 发送图片、小视频、语音 * @param fileType Int * @param picPath String * @param toUserID String */ fun sendFile(fileType: Int, filePath : String, toUserId : String, time: Int) { CoroutineScope(Dispatchers.IO).launch { //语音、小视频多少秒 val toId: String = BaseUtils.getChatId(toUserId) + "/Smack" var resultFilePath = "" if(TextUtils.isEmpty(filePath)){ return@launch } if(!File(filePath).exists()){ return@launch } var second = time if(fileType == ChatBean.CONTENT_TYPE_VOICE){ second = time } else if(fileType == ChatBean.CONTENT_TYPE_VIDEO){ second = CommonUtils.Media.getMediaTime(filePath, fileType) } //先刷新页面,再慢慢上传文件 var chatBean = processSendChatBean(toUserId, fileType, filePath, second) if(fileType == ChatBean.CONTENT_TYPE_IMG){ //图片 //压缩图片后路径 var resultPicPath = UploadFileUtils.getCompressFile(filePath) resultFilePath = resultPicPath } else if(fileType == ChatBean.CONTENT_TYPE_VOICE){ //语音 resultFilePath = filePath TagUtils.d("语音时间:${second}") } else if(fileType == ChatBean.CONTENT_TYPE_VIDEO){ //小视频 var videoFilePath = UploadFileUtils.getVideoCompressFile() //TagUtils.d("开始发送小视频压缩前文件2:${videoFilePath}") var compressResult = VideoController.getInstance().convertVideo(filePath, videoFilePath, VideoController.COMPRESS_QUALITY_LOW, object : VideoController.CompressProgressListener { override fun onProgress(percent: Float) { //TagUtils.d("压缩小视频进度:${percent}") } }) TagUtils.d("小视频时间:${second}") resultFilePath = videoFilePath } TagUtils.d("上传文件:${resultFilePath}") val filetosend = File(resultFilePath) if (!filetosend.exists()) { return@launch } try { var account : String = DataStoreUtils.getAccount() if(fileType == ChatBean.CONTENT_TYPE_IMG){ //图片 TagUtils.d("图片发送成功。") var chatBeanServer = UploadFileUtils.uploadChatImages(account, toUserId, resultFilePath,0) if(chatBeanServer != null){ chatBeanServer.imgPath = chatBeanServer.imgPath chatBean = chatBeanServer } } else if(fileType == ChatBean.CONTENT_TYPE_VOICE){ //录音完成,要转码,等待0.2秒再发送 delay(100) //语音 var chatBeanServer = UploadFileUtils.uploadChatVoice(account, toUserId, resultFilePath, second) if(chatBeanServer != null){ chatBeanServer.voiceLocal = resultFilePath chatBean = chatBeanServer } } else if(fileType == ChatBean.CONTENT_TYPE_VIDEO){ //小视频 var chatBeanServer = UploadFileUtils.uploadChatVideo(account, toUserId, resultFilePath, second) if(chatBeanServer != null){ chatBeanServer.videoLocal = resultFilePath chatBean = chatBeanServer } } chatBean?.let { ChatRepository.updateChat(it) } var baseSystemBoolean = BaseSystemRepository.getBaseSystemSync(WcApp.getContext().packageName) if(baseSystemBoolean != null && baseSystemBoolean.sync == CommonUtils.Sync.SERVER_SYNC){ //同步不需要使用transfer上传文件,因为transfer上传太慢了, 直接上传到服务器,然后发送普通的消息 if(fileType == ChatBean.CONTENT_TYPE_VOICE){ var content = CommonUtils.Chat.VOICE_MARK + chatBean.voice ChatManagerUtils.getInstance().sendMessage(toUserId, content) } else if(fileType == ChatBean.CONTENT_TYPE_VIDEO){ var content = CommonUtils.Chat.VIDEO_MARK + chatBean.video ChatManagerUtils.getInstance().sendMessage(toUserId, content) } else if(fileType == ChatBean.CONTENT_TYPE_IMG){ var content = CommonUtils.Chat.IMAGE_MARK + chatBean.imgPath ChatManagerUtils.getInstance().sendMessage(toUserId, content) } return@launch } val fileTransferManager = getFileTransferManager() val transfer = fileTransferManager.createOutgoingFileTransfer(toId) // 创建一个输出文件传输对象 //对方用户在线才需要上传文件 if(XmppConnectionManager.getInstance().isOnLine(toUserId)){ TagUtils.d("${toUserId} 在线 开始上传。") transfer.sendFile(filetosend,"recv img") while (!transfer.isDone) { if (transfer.status == FileTransfer.Status.error) { TagUtils.d("聊天文件上传错误 , ERROR!!! " + transfer.error) } else { TagUtils.d("聊天文件上传 " + transfer.status +" , " + transfer.progress) } Thread.sleep(20) } } else { TagUtils.d("toUserId 不在线") } if (transfer.isDone) { //上传完成 } } catch (e1: XMPPException) { e1.printStackTrace() } } }
//接收图片、语音、小视频文件监听 fun addFileListerer() { val manager = getFileTransferManager() manager.addFileTransferListener { request -> CoroutineScope(Dispatchers.IO).launch { //文件接收 val transfer = request.accept() OutgoingFileTransfer.getResponseTimeout() //获取文件名字 val fileName = transfer.fileName TagUtils.d("接收图片、语音:${fileName}") //本地创建文件 val sdCardDir = File(FileUtils.CHAT_ALBUM_PATH) if (!sdCardDir.exists()) { //判断文件夹目录是否存在 sdCardDir.mkdir() //如果不存在则创建 } //val savePath: String = FileUtils.CHAT_ALBUM_PATH + fileName val savePath: String = FileUtils.getBaseFile(fileName).path val file = File(savePath) //接收文件 try { transfer.recieveFile(file) while (!transfer.isDone) { if (transfer.status == FileTransfer.Status.error) { TagUtils.d("接收、语音文件 ERROR!!! " + transfer.error) } else { TagUtils.d("接收、语音文件" + transfer.status + " , " + transfer.progress) } //接收小视频,要停顿一下,否则小视频不完整打不开。 Thread.sleep(20) } if (transfer.isDone) { var account = DataStoreUtils.getAccount() var chatBean : ChatBean? = null var userType = ChatBean.USER_TYPE_OTHER var peer = transfer.peer TagUtils.d("接收文件完成peer:${peer}") var toUserId = BaseUtils.getChatAccountFrom(peer) TagUtils.d("接收文件完成toUserId:${toUserId}, account = ${account}") if(fileName.endsWith(".mp3", true)){ //接收语音 var second = CommonUtils.Media.getMediaTime(savePath, ChatBean.CONTENT_TYPE_VOICE) chatBean = CommonUtils.Chat.getChatBeanVoice(toUserId, account, userType, ChatBean.CONTENT_TYPE_VOICE, savePath, second) } else if(fileName.endsWith(".mp4", true)){ //接收小视频 var second = CommonUtils.Media.getMediaTime(file.path, ChatBean.CONTENT_TYPE_VIDEO) TagUtils.d("接收的小视频文件:${savePath}, ${second}秒") chatBean = CommonUtils.Chat.getChatBeanVideo(toUserId, account, userType, ChatBean.CONTENT_TYPE_VIDEO, savePath, second) } else { //接收图片 chatBean = CommonUtils.Chat.getChatBean(toUserId, account, userType, "", ChatBean.CONTENT_TYPE_IMG, savePath,0.0, 0.0) } chatBean?.let { ChatRepository.insertChat(chatBean) EventBus.getDefault().post(chatBean) } } } catch (e: XMPPException) { e.printStackTrace() } } } }
//web服务器方式下载 override fun chatCreated(chat: Chat, createdLocally: Boolean) { TagUtils.d("消息监听回调:chat = ${chat} , createdLocally = ${createdLocally}") if(!createdLocally){ chat.addMessageListener { chat, message -> TagUtils.d("获取好友发来的信息 ${message.from} , ${message.to}, ${message.body}") var content = message.getBody() if(!TextUtils.isEmpty(content) && content.length > 0){ var fromUser = BaseUtils.getChatAccountFrom(message.from) var toUser = BaseUtils.getChatAccount(message.to) var userType = ChatBean.USER_TYPE_OTHER if(content.startsWith(CommonUtils.Chat.LOCATION_MARK)){ //发送定位 //去掉location###标志 var remarkContent = CommonUtils.Chat.getLocation(content) //使用逗号分隔符,分别读取经纬度 var contents = remarkContent.split(",") var latitude = contents[0].toDouble() var longitude = contents[1].toDouble() var chatBean = CommonUtils.Chat.getChatBean(fromUser, toUser, userType, content, ChatBean.CONTENT_TYPE_LOCATION, "", latitude, longitude) ChatRepository.insertChat(chatBean) chatBean.isReceive = true EventBus.getDefault().post(chatBean) } else if(content.startsWith(CommonUtils.Chat.REDPACKET_MARK)){ //发送红包, 去掉redpacket###写入数据库 content = CommonUtils.Chat.getRedpacket(content).toString() var chatBean = CommonUtils.Chat.getChatBean(fromUser, toUser, userType, content, ChatBean.CONTENT_TYPE_REDPACKET, "",0.0, 0.0) ChatRepository.insertChat(chatBean) chatBean.isReceive = true EventBus.getDefault().post(chatBean) } else if(content.startsWith(CommonUtils.Chat.VOICE_MARK)){ //发送语音, 去掉voice###写入数据库 content = CommonUtils.Chat.getMedia(content, CommonUtils.Chat.VOICE_MARK) var chatBean = CommonUtils.Chat.getChatBeanVoiceServer(fromUser, toUser, userType,ChatBean.CONTENT_TYPE_VOICE, content,0) chatBean.isReceive = true processDownload(chatBean) } else if(content.startsWith(CommonUtils.Chat.VIDEO_MARK)){ //发送小视频, 去掉video###写入数据库 content = CommonUtils.Chat.getMedia(content, CommonUtils.Chat.VIDEO_MARK) var chatBean = CommonUtils.Chat.getChatBeanVideoServer(fromUser, toUser, userType,ChatBean.CONTENT_TYPE_VIDEO, content,0) chatBean.isReceive = true processDownload(chatBean) } else if(content.startsWith(CommonUtils.Chat.IMAGE_MARK)){ //发送图片, 去掉image###写入数据库 content = CommonUtils.Chat.getMedia(content, CommonUtils.Chat.IMAGE_MARK) var chatBean = CommonUtils.Chat.getChatBeanImageServer(fromUser, toUser, userType,ChatBean.CONTENT_TYPE_IMG, content,0) chatBean.isReceive = true processDownload(chatBean) } else if(content.startsWith(CommonUtils.Chat.TRANSFER_MARK)){ //发送转账, 去掉transfer###写入数据库 content = CommonUtils.Chat.getTransfer(content).toString() var chatBean = CommonUtils.Chat.getChatBean(fromUser, toUser, userType, content, ChatBean.CONTENT_TYPE_TRANSFER, "",0.0, 0.0) ChatRepository.insertChat(chatBean) chatBean.isReceive = true EventBus.getDefault().post(chatBean) } else if(content.startsWith(CommonUtils.QRCommon.QR_RECEIVE_CODE)){ //向个人发送收款 var balance = content.substring(CommonUtils.QRCommon.QR_RECEIVE_CODE.length, content.length) TagUtils.d("MyChatManagerListener 向个人发送收款金额: ${fromUser} , ${toUser}, ${balance}") updateBalanceServer(fromUser, toUser, CommonUtils.User.OPERATOR_PLUS, balance.toFloat()) } else if(content.startsWith(CommonUtils.QRCommon.QR_PAYMENT_CODE)){ //向商家付款 var balance = content.substring(CommonUtils.QRCommon.QR_RECEIVE_CODE.length, content.length) TagUtils.d("MyChatManagerListener 向商家付款金额: ${fromUser} , ${toUser}, ${balance}") updateBalanceServer(fromUser, toUser, CommonUtils.User.OPERATOR_MINUS, balance.toFloat()) } else { var chatBean = CommonUtils.Chat.getChatBean(fromUser, toUser, userType, content, ChatBean.CONTENT_TYPE_TEXT, "",0.0, 0.0) ChatRepository.insertChat(chatBean) chatBean.isReceive = true EventBus.getDefault().post(chatBean) } ChatNotificationUtils.sendNotification(fromUser) } } } } /** * 下载图片、语音、小视频 */ private fun processDownload(chatBean : ChatBean){ var fileName = "" if(chatBean.contentType == ChatBean.CONTENT_TYPE_VOICE){ fileName = chatBean.voice } else if(chatBean.contentType == ChatBean.CONTENT_TYPE_VIDEO){ fileName = chatBean.video } else if(chatBean.contentType == ChatBean.CONTENT_TYPE_IMG){ fileName = chatBean.imgPath } else { return } var videoUrl = CommonUtils.Moments.getReallyImageUrl(fileName) var videoFile = FileUtils.getBaseFile(fileName) TagUtils.d("下载多媒体Url :${videoUrl} ") TagUtils.d("下载多媒体File :${videoFile} ") VideoDownloadManager.downloadFast(videoUrl, videoFile, object : VideoDownloadInter { override fun onDone(filePath: String) { TagUtils.d("小视频多媒体完成:${filePath}") if(chatBean.contentType == ChatBean.CONTENT_TYPE_VOICE){ var second = CommonUtils.Media.getMediaTime(filePath, chatBean.contentType) chatBean.second = second chatBean.voiceLocal = filePath } else if(chatBean.contentType == ChatBean.CONTENT_TYPE_VIDEO){ chatBean.videoLocal = filePath var second = CommonUtils.Media.getMediaTime(filePath, chatBean.contentType) chatBean.second = second } else if(chatBean.contentType == ChatBean.CONTENT_TYPE_IMG){ chatBean.imgPathLocal = filePath } ChatRepository.insertChat(chatBean) EventBus.getDefault().post(chatBean) } override fun onError() { } override fun onProgress(process: Int) { } }) }