鸿蒙开发仿微信长按录音效果
如果你项目有IM聊天,那么长按录音功能是必须的,最好是跟微信一样的效果,对不对。
一、思路:
自定义触碰事件
二、效果图:
鸿蒙开发教程实战案例源码分享-仿微信长按录音效果
三、关键代码:
// 联系:893151960
@Entry
@Component
struct Index {
// 录音聊天声音
static readonly MAX_AMPLITUDE: number = 15000;
static readonly MIN_AMPLITUDE: number = 1500;
static readonly COLUMN_HEIGHT: number = 100;
static readonly HEIGHT_MIN: number = 60;
// 播放语音的播放器
// 区分语音还是文字输入状态
@State isVoiceInput:boolean = false
microphonePermissions: Permissions = 'ohos.permission.MICROPHONE';
hasMicrophonePermissions:boolean = false
// 是否正在请求麦克风权限
isRequestMicrophonePermissions:boolean = false
// Column高度最大值,用于长按是声音的振幅
@State yMax: number = 0;
// Column高度最小值
@State yMin: number = 0;
// 声音文件名称,不能重复,否则播放自己本地语音,只会播放最后一条
voiceName:string = ''
// 当前录音时间,用于录制到50s后倒计时
currentRecordVoiceTime:number = 0
@State countDownVoiceText:string = ''
maxMicVolumeCountId: number = 0
// 开始录制时的时间戳
audioTapTs: number = 0;
audioFile: fs.File | null = null;
// 创建音频录制与播放实例
avPlayer?:media.AVPlayer
@State voicePlayState: AnimationStatus = AnimationStatus.Initial
@State voicePlayMessageId:number = 0
// 按住说话 录音模态
@State
showTalkContainer: boolean = false
// 长按状态
@State
pressCancelVoicePostText: PressCancelVoicePostText = PressCancelVoicePostText.none
// “x ”的坐标
xScreenOffset: ScreenOffset = {
x: 0,
y: 0,
width: 0,
height: 0
}
// 按住说话 持续触发
onPressTalk = async (event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.currentRecordVoiceTime = 0
this.countDownVoiceText = ''
PermissionUtil.checkPermissions(this.microphonePermissions).then((result:boolean) => {
if (result) {
// 有权限
this.hasMicrophonePermissions = true
this.isRequestMicrophonePermissions = false
// 手指按下时触发
this.pressCancelVoicePostText = PressCancelVoicePostText.presssing
// 按下
this.showTalkContainer = true
// 开始录音
this.startRecordVoice()
} else {
this.isRequestMicrophonePermissions = true
// 问麦克风权限
PermissionUtil.requestPermissionsEasy(this.microphonePermissions).then((result:boolean)=>{
if (result) {
// 有权限
this.hasMicrophonePermissions = true
// 这里先不录,因为用户放开手去点击允许权限了
//this.startRecordVoice()
} else {
this.hasMicrophonePermissions = false
}
})
}
})
} else if (event.type === TouchType.Move) {
if (this.hasMicrophonePermissions && !this.isRequestMicrophonePermissions) {
// 手指移动时持续触发
this.pressCancelVoicePostText = PressCancelVoicePostText.presssing
// 获取当前手指的坐标
const x = event.touches[0].displayX
const y = event.touches[0].displayY
// 判断是否碰到了 “X”
let isTouchX = this.xScreenOffset.x <= x && this.xScreenOffset.x + this.xScreenOffset.width >= x &&
this.xScreenOffset.y <= y && this.xScreenOffset.y + this.xScreenOffset.width >= y
if (isTouchX) {
// 取消发送
this.pressCancelVoicePostText = PressCancelVoicePostText.cancelVoice
}
}
} else if (event.type === TouchType.Up) {
// 松开手
if (this.showTalkContainer) {
this.showTalkContainer = false
clearInterval(this.maxMicVolumeCountId)
animateTo({ duration: 0 }, () => {
this.yMax = 0
this.yMin = 0
})
if (this.hasMicrophonePermissions && !this.isRequestMicrophonePermissions) {
if (this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice) {
// 取消发送
AudioRecorder.getInstance().stopRecordingProcess()
} else {
// 发送录音
this.recordAudioEndAndSendMessage()
}
}
}
}
}
/**
* @desc : 开始录音
* @author : congge on 2024-09-05 17:11
**/
startRecordVoice(){
// 获取时间戳
this.voiceName = Date.now().toString()
this.recordAudioStart(this.voiceName)
// 每隔100ms获取一次振幅
this.maxMicVolumeCountId = setInterval(() => {
AudioRecorder.getInstance().avRecorder?.getAudioCapturerMaxAmplitude((_: BusinessError, amplitude: number) => {
let maxNumber: number = 0 // Column最大高度
let minNumber: number = 0 // Column最小高度
if (amplitude > Index.MIN_AMPLITUDE) {
maxNumber = amplitude / Index.MAX_AMPLITUDE * Index.COLUMN_HEIGHT
minNumber = amplitude / Index.MAX_AMPLITUDE * Index.COLUMN_HEIGHT - Index.HEIGHT_MIN
}
if (this.showTalkContainer) {
animateTo({ duration: 500, curve: Curve.EaseInOut }, () => {
this.yMax = maxNumber
this.yMin = minNumber
})
}
})
this.currentRecordVoiceTime = this.currentRecordVoiceTime + 0.1;
if (this.currentRecordVoiceTime >= 60){
this.showTalkContainer = false
clearInterval(this.maxMicVolumeCountId)
animateTo({ duration: 0 }, () => {
this.yMax = 0
this.yMin = 0
})
this.recordAudioEndAndSendMessage()
} else if (this.currentRecordVoiceTime > 50) {
this.countDownVoiceText = `${Math.floor(60-this.currentRecordVoiceTime)}`
} else {
this.countDownVoiceText = ''
}
}, 100);
}
private async recordAudioStart(name:string) {
this.audioTapTs = Date.now();
let fdStr = "fd://" + this.getAudioFileFd(name);
await AudioRecorder.getInstance().startRecordingProcess(fdStr);
}
private getAudioPath(name:string): string {
let audioDir = getContext().filesDir;
let audioPath = audioDir +"/"+name+ ".aac";
return audioPath;
}
private getAudioFileFd(name:string): number {
let audioPath = this.getAudioPath(name);
let file = fs.openSync(audioPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
this.audioFile = file;
return file.fd;
}
/**
* @desc : 录制结束并且发送消息
* @author : congge on 2024-09-07 10:11
**/
private async recordAudioEndAndSendMessage() {
let delta = 0;
if (this.audioTapTs > 0) {
delta = (Date.now() - this.audioTapTs)/ 1000;
delta = Math.floor(delta)
}
await AudioRecorder.getInstance().stopRecordingProcess();
if (delta >= 1) {
if (delta > 60) {
showToast($r('app.string.rc_voice_too_long'))
} else {
fs.close(this.audioFile);
// 发送语音消息
//ImUtils.sendVoiceMessage(this.targetId,this.getAudioPath(this.voiceName),delta)
}
} else {
showToast($r('app.string.rc_voice_short'))
}
}
// 正在说话 页面布局
@Builder
TalkContainerBuilder() {
Column() {
// 1 中心的提示 显示波浪线
Column() {
// 声纹
ButtonWithWaterRipples({ yMax: this.yMax, yMin: this.yMin });
if (this.countDownVoiceText){
Text(`${this.countDownVoiceText}"后将停止录音`)
.fontColor('#ffffff')
.margin({
top:10
})
}
}
.height(80)
.width("50%")
.backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? Color.Red : "#95EC6A")
.position({
top: "40%",
left: "50%"
})
.translate({
x: "-50%"
})
.borderRadius(10)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
// 2 取消和转文字
Column({space:30}) {
// 3 松开发送
Text(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? '取消发送' : "松开发送")
.fontColor("#fff")
.width("100%")
.textAlign(TextAlign.Center)
Text("X")
.fontSize(20)
.width(60)
.height(60)
.borderRadius(30)
.fontColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#000" : "#ccc")
.backgroundColor(this.pressCancelVoicePostText === PressCancelVoicePostText.cancelVoice ? "#fff" : "#333")
.textAlign(TextAlign.Center)
.align(Alignment.Center)
.fontColor("#ccc")
.id("aabb")
.onAppear(() => {
let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById("aabb");
this.xScreenOffset.x = px2vp(modePosition.screenOffset.x)
this.xScreenOffset.y = px2vp(modePosition.screenOffset.y)
this.xScreenOffset.width = px2vp(modePosition.size.width)
this.xScreenOffset.height = px2vp(modePosition.size.height)
})
}
.width("100%")
.position({
bottom: "23%"
})
.padding({
left: 60, right: 60
})
// 4 底部白色大球
Row() {
}
.width(600)
.height(600)
.backgroundColor("#fff")
.position({
bottom: 0,
left: "50%"
})
.translate({
x: "-50%",
y: "70%"
})
.borderRadius("50%")
}
.width("100%")
.height("100%")
.backgroundColor("rgba(0,0,0,0.5)")
}
build() {
RelativeContainer() {
Text($r("app.string.voice_record_dynamic_effect_button"))
.height(40)
.width('100%')
.borderRadius(20)
.focusable(true)
.textAlign(TextAlign.Center)
.backgroundColor('#F1F3F5')
.fontColor(Color.Black)
.bindContentCover($$this.showTalkContainer, this.TalkContainerBuilder,
{ modalTransition: ModalTransition.NONE })
.onTouch(this.onPressTalk)
.alignRules({
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}
四、项目demo源码结构图:
有问题或者需要完整源码的私信我