鸿蒙开发教程实战案例源码分享-仿微信长按录音效果

鸿蒙开发仿微信长按录音效果

如果你项目有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源码结构图:

在这里插入图片描述有问题或者需要完整源码的私信我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值