【uni-app】模仿微信实现简易发送/取发语音功能

学习uni-app开发,实现了一个微信聊天的demo,简单记录下其中的语音发送功能。这里只是介绍从发送到显示的过程,暂不涉及websocket做聊天对话。若有错误和不足之处留言指正,谢谢!

功能就不细说了,最终效果的UI界面如下,用户昵称头像皆为测试数据。

长按按住说话按钮弹出遮罩层按住说话,直接松开即可发送语音。

若向左上方滑动松开后取消发送当前语音。

一. 绑定事件 

首先给按钮是绑定事件,需要给按钮绑定三个事件

1.touchstart事件会在触摸按钮时触发,可以获取手指的初始坐标;

2.touchmove事件在触摸后移动手指时触发,可以用来计算手手指的滑动距离;

3.touchend事件在松开手指时候触发。

<button type="default" 
 v-show="mode === 'voice'" 
 @touchstart="handleTouchStart"
 @touchmove="handleTouchMove"
 @touchend="handleTouchEnd"
>按住 说话</button>

页面遮罩层

<!-- 语音遮罩层 -->
<view class="voice-mask" v-show="mask">
	<!--语音条 -->
	<view class="voice-bar voice-del" :class="{voiceDel:needCancel}" :style={width:getVoiceBarWidth}>
		<image src="../static/icon/wave.png" class="voice-volume" :class="{volumeDel:needCancel}"></image>
		<view class="trangle-bottom" :class="{trangleDel:needCancel}"></view>
	</view>
	<!-- 底部区域 -->
	<view class="voice-send">
		<!-- 取消和转文字图标 -->
		<view class="voice-middle-wrapper">
			<!-- 取消 -->
			<view class="voice-left-wrapper">
				<view class="cancel-del" :class="{delTip:needCancel}">松开 取消</view>
				<view class="voice-middle-inner close" :class="{bigger:needCancel}">
					<image src="../static/icon/close-grey.png" class="close-icon"></image>
				</view>
			</view>
			<!-- 转文字 -->
			<view class="voice-middle-inner to-text">
				<text class="wen">文</text>
				<!-- <image src="" class="wen"></image> -->
			</view>
			<view class="send-tip" :class="{sendTipNone:needCancel}">松开 发送</view>
		</view>	
		<!-- 底部语音按钮 -->
		<view class="mask-bottom">
			<image src="../static/icon/voice-left.png"></image>
		</view>
	</view>	
</view>


......

<style>
   .voice-mask{
		position:fixed;
		top:0;
		right:0;
		bottom:0;
		left:0;
		/* display: flex;
		flex-direction: column;
		justify-content: flex-end;
		align-items: center; */
		background-color: rgba(0,0,0,0.8);
	}
	.voice-bar{
		position: absolute;
		left:50%;
		top: 50%;
		transform: translate(-50%,-30%);
		/* width: 230rpx; */
		height:150rpx;
		background-color:#51ff50;
		border-radius: 26rpx;
		margin-bottom: 220rpx;
	}
	.voiceDel{
		left:80rpx;
		top: 50%;
		width: 170rpx !important;
		transform: translateX(0%);
		transform: translateY(-30%);
		background-color: red;
	}
	.voice-volume{
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%,-50%);
		width: 160rpx;
		height: 36rpx;
	}
	.volumeDel{
		width: 80rpx;
	}
	.trangle-bottom{
		position: absolute;
		bottom: -38rpx;
		left:50%;
		transform: translateX(-50%);
		border-width: 20rpx;
		border-style: solid;
		border-color: #51FF50 transparent transparent transparent;
	}
	.trangleDel{
		border-color: red transparent transparent transparent;
	}
	.voice-send{
		position: absolute;
		bottom: 0;
		width: 100%;
	}
	.voice-middle-wrapper{
		width: 100%;
		display: flex;
		position:relative;
		justify-content: space-between;
		align-items: flex-end;
		margin-bottom: 40rpx;
	}
	.voice-left-wrapper{
		display: flex;
		flex-direction: column;
		justify-content: center;
		align-items: flex-end;
	}
	.cancel-del{
		display:none;
	}
	.delTip{
		display:block;
		color:#bfbfbf;
		margin: 0 22rpx 18rpx 0;
	}
	.voice-middle-inner{
		display: flex;
		justify-content: center;
		align-items: center;
		background-color: rgba(0,0,0,0.2);
		width: 140rpx;
		height: 140rpx;
		border-radius: 50%;
	}
	.close{
		transform: rotate(350deg);
		margin-left: 80rpx;
	}	
	.bigger{
		width: 170rpx;
		height: 170rpx;
	}
	.to-text{
		transform: rotate(10deg);
		margin-right: 80rpx;
	}
	.close-icon{
		width: 80rpx;
		height: 80rpx;	
	}
	.wen{
		font-size: 40rpx;
		color:#bfbfbf;
	}
	.send-tip{
		position: absolute;
		left: 50%;
		bottom:0rpx;
		transform: translate(-50%,36%);
		color:#bfbfbf;
	}
	.sendTipNone{
		display: none;
	}
	.mask-bottom{
		position: relative;
		width: 100%;
		height:190rpx;
		border-top: #BABABB 8rpx solid;
		border-radius: 300rpx 300rpx 0 0;
		background-image: linear-gradient(#949794,#e1e3e1);
	}
	.mask-bottom image{
		position: absolute;
		width: 60rpx;
		height: 60rpx;
		top: 0;
		right:0;
		bottom: 0;
		left: 0;
		margin: auto;
	}
</style>

随后定义事件回调, 三个事件中可以在事件对象event中touches数组获取当前屏幕上所有触摸点的列表,touches数组中保存着多个手指触摸点的信息,触摸点信息中的pageX,pageY属性获取的是触摸目标在页面中的x和y坐标。

二.音频录制

录制语音需要获取uni-app提供的全局录音管理器uni.getRecorderManager();

官方文档:录音管理 - uni-app官网 (dcloud.io)

开始录音调用该对象的recorderManager.start()方法,结束录音调用recorderManager.stop(),并在结束的回调recorderManager.onStop中获取音频的文件地址。

开始录音时需要记录保存初始的触摸坐标pageX和pageY到当前组件实例,此外由于语音需要显示时长,还需要开启计时器setInterval计算录音时长,长度保存在组件实例的length属性中,且录音的时长上限为59s。

const recorderManager = uni.getRecorderManager();
// 开始录制语音
handleTouchStart(e){
	this.mask = true;
	recorderManager.start();
	this.length = 1;
	this.startX = e.touches[0].pageX;
	this.startY = e.touches[0].pageY;
	this.timer = setInterval(() => {
		this.length += 1;
		if(this.length >= 60) {
			clearInterval(this.timer);
			this.handleTouchEnd()
		}
	},1000);
},

此外还需实现了一个细节,遮罩层中的语音条的长度是随录制时长递增的,如下图:

 使用计算属性实现,录音时长和长度关系如下:

computed:{
	// 计算语音条宽度
	getVoiceBarWidth(){
		return (230 + this.length * 4) + 'rpx';
	}
},

三.取消发送行为判断

开始录制语音后开启遮罩层,此时若用户移动手指会触发touchmove事件,在该事件的事件对象中获取当前用户手指在页面的实时坐标pageX和pageY,判断用户是否存在向左上方方向移动手指至取消发送图标行为,这里做了一些测试,取this.startX - e.touches[0].pageX > 14 && this.startY - e.touches[0].pageY > 50为判断条件,满足条件即为取消发送。

// 语音录制时滑动事件
handleTouchMove(e){
	if(this.startX - e.touches[0].pageX > 14 && this.startY - e.touches[0].pageY > 50){
		this.needCancel = true;
	} else {
		this.needCancel = false;
	}
},

四.提交音频

用户松开手指执行录制结束的回调,其中需要根据needCancel 标志判断该音频是否需要发送;在onStop回调中整理音频信息,调用提交音频的方法。

// 语音录制结束
handleTouchEnd(){
	this.mask = false;
	clearInterval(this.timer);
	recorderManager.stop();
	recorderManager.onStop((res) => {
		const message = {
			voice:res.tempFilePath,
			length:this.length
		};
		if(!this.needCancel){
			this.inputSubmit(message,2);
		}
		this.needCancel = false
	});
}

这里将聊天界面底部菜单栏单独封装成一个组件bottomBar,所以在音频录制完毕后将信息提交父组件chatroom遍历展示。

inputSubmit(msg,types){
	if(msg.types === 0 && this.inputText == '') return;
	this.$emit('sub',msg,types);	
	if(this.inputText){
		this.inputText = '';
	}
},

语音条的页面结构如下所示

<!-- 2 - 当前用户的语音 -->
<view class="chat-voice-right" :style="{width:handleVoiceWidth(item.message.length)}" v-if="item.types === '2'" @tap="handleVoicePlay(item)">
	<view class="chat-voice-right-inner">
		<!-- 时长 -->
		<text decode='true' class="chat-voice-length-right">{
   
   {item.message.length}}</text>
		<!-- 语音条主体 -->
		<text class="chat-voice-second-right">"</text>
		<!-- 语音图标 -->
		<image src="../../static/icon/voice-right.png" class="voice-img"></image>
	</view>
	<!-- 向右三角形 -->
	<view class="trangle trangle-right"></view>
</view>

  

六.音频长度处理 

语音条长度和时长之间存在一个转换关系,10秒内的语音和大于10秒的语音长度增长比例是不一致的,这里规定在10秒的语音长度正好为可变长度的一半,随后的50秒缓慢增长到可变长度的最大值Lmax,具体转换关系如下:

// 处理语音长度
handleVoiceWidth(lenght){
	lenght = lenght - 1;
	let Lmin = 138;
	let Lmax = 366
	let barCanChangeLen = Lmax - Lmin;
	
	// 11秒以内的语音
	if (lenght < 11) {
		// VoicePlayTimes 为10秒时,正好为可变长度的一半
		return (Lmin + lenght * 0.05 * barCanChangeLen) + 'rpx'; 
	} else {
		// 12-60秒的语音
		return (Lmin + 0.5 * barCanChangeLen + (lenght - 10) * 0.01 * barCanChangeLen) + 'rpx';
	}			
},

七.音频播放

播放语音需要创建并返回内部 audio 上下文 innerAudioContext 对象;

官方文档:音频组件控制 - uni-app官网 (dcloud.io)

需要指定src属性为播放音频的链接,调用play()播放,调用stop()停止,播放自然结束触发触发方法onEnded,onStop修改播放标志。

const innerAudioContext = uni.createInnerAudioContext();
// 播放语音
handleVoicePlay(item){
	item.isFirstPlay = false;
	innerAudioContext.src = item.message.voice;
	this.isPlay = !this.isPlay;
	this.isPlay ? innerAudioContext.play() : innerAudioContext.stop();
	innerAudioContext.onEnded(() => {
		this.isPlay = false;
	})
    innerAudioContext.onStop(() => {
		this.isPlay = false;
	})
},

猜你喜欢

转载自blog.csdn.net/weixin_43655896/article/details/122905513