种地 发表于 2025-3-28 17:32:01

鸿蒙殊效教程02-微信语音录制动画

鸿蒙殊效教程02-微信语音录制动画效果实现教程

   本教程适合HarmonyOS初学者,通过简单到复杂的步调,一步步实现类似微信APP中的语音录制动画效果。
开辟环境准备



[*]DevEco Studio 5.0.3
[*]HarmonyOS Next API 15
终极效果预览

我们将实现以下功能:

[*]长按"按住语言"按钮:显示录音界面和声颠簸画
[*]录音过程中显示及时时长
[*]手指上滑:取消录音发送
[*]松开手指:根据状态发送或取消录音
https://i-blog.csdnimg.cn/direct/bf4841f9b5554b5daef6e98840f2db7a.gif
一、底子布局实现

首先,我们须要创建根本的界面布局,模拟微信谈天界面的结构。
@Entry
@Component
struct WeChatRecorder {
build() {
    Column() {
      // 聊天内容区域(模拟)
      Stack({ alignContent: Alignment.Center }) {
      }
      .layoutWeight(1)
      
      // 底部输入栏
      Row() {
      // 录音按钮
      Text('按住 说话')
          .fontSize(16)
          .fontColor('#333333')
          .backgroundColor('#F5F5F5')
          .borderRadius(4)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(40)
          .padding({ left: 10, right: 10 })
      }
      .width('100%')
      .backgroundColor(Color.White)
      .expandSafeArea()
      .padding({ left: 15, right: 15, top: 15 })
      .border({ width: { top: 1 }, color: '#E5E5E5' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#EDEDED')
    .expandSafeArea()
}
}
这一步我们创建了一个根本的谈天界面布局,包含两部分:

[*]顶部谈天内容地区:利用Stack布局,现在为空
[*]底部输入栏:包含一个"按住 语言"按钮
二、添加状态变量

接下来,我们须要添加一些状态变量来跟踪录音状态和动画效果。
@Entry
@Component
struct WeChatRecorder {
// 是否正在录音
@State isRecording: boolean = false
// 是否显示取消提示(上滑状态)
@State isCancel: boolean = false
// 录音时长(秒)
@State recordTime: number = 0
// 声波高度变化数组
@State waveHeights: number[] =
// 计时器ID
private timerId: number = 0
// 波形动画计时器ID
private waveTimerId: number = 0
// 触摸起始位置
private touchStartY: number = 0
// 触摸移动阈值,超过该值显示取消提示
private readonly cancelThreshold: number = 50

build() {
    // 之前的布局代码
}
}
我们添加了以下状态变量:

[*]isRecording:跟踪是否正在录音
[*]isCancel:跟踪是否处于取消录音状态(上滑)
[*]recordTime:纪录录音时长(秒)
[*]waveHeights:存储声波高度数组,用于实现波形动画
[*]timerId:存储计时器ID,用于后续扫除
[*]waveTimerId:存储波形动画计时器ID
[*]touchStartY:纪录触摸起始位置,用于盘算上滑距离
[*]cancelThreshold:定义上滑多少距离触发取消状态
三、添加底子方法

在实现UI交互前,我们先添加一些底子方法来处理录音状态和动画效果。
@Entry
@Component
struct WeChatRecorder {
// 状态变量定义...

/**
   * 开始录音,初始化状态及启动计时器
   */
startRecording() {
    this.isRecording = true
    this.isCancel = false
    this.recordTime = 0

    // 启动计时器,每秒更新录音时长
    this.timerId = setInterval(() => {
      this.recordTime++
    }, 1000)

    // 启动波形动画计时器,随机更新波形高度
    this.waveTimerId = setInterval(() => {
      this.updateWaveHeights()
    }, 200)
}

/**
   * 结束录音,清理计时器和状态
   */
stopRecording() {
    // 清除计时器
    if (this.timerId !== 0) {
      clearInterval(this.timerId)
      this.timerId = 0
    }

    if (this.waveTimerId !== 0) {
      clearInterval(this.waveTimerId)
      this.waveTimerId = 0
    }

    // 如果是取消状态,则显示取消提示
    if (this.isCancel) {
      console.info('录音已取消')
    } else if (this.recordTime > 0) {
      // 如果录音时长大于0,则模拟发送语音
      console.info(`发送语音,时长: ${this.recordTime}秒`)
    }

    // 重置状态
    this.isRecording = false
    this.isCancel = false
    this.recordTime = 0
}

/**
   * 更新波形高度以产生动画效果
   */
updateWaveHeights() {
    // 创建新的波形高度数组
    const newHeights = this.waveHeights.map(() => {
      // 生成20-40之间的随机高度
      return Math.floor(Math.random() * 20) + 20
    })

    this.waveHeights = newHeights
}

/**
   * 格式化时间显示,将秒转换为"00:00"格式
   */
formatTime(seconds: number): string {
    const minutes = Math.floor(seconds / 60)
    const secs = seconds % 60
    return `${minutes.toString()
      .padStart(2, '0')}:${secs.toString()
      .padStart(2, '0')}`
}

aboutToDisappear() {
    // 组件销毁时清除计时器
    if (this.timerId !== 0) {
      clearInterval(this.timerId)
      this.timerId = 0
    }

    if (this.waveTimerId !== 0) {
      clearInterval(this.waveTimerId)
      this.waveTimerId = 0
    }
}

build() {
    // 之前的布局代码
}
}
在这一步中,我们实现了以下方法:

[*]startRecording:开始录音,初始化状态并启动计时器
[*]stopRecording:竣事录音,清理计时器和状态
[*]updateWaveHeights:更新波形高度数组,产生动画效果
[*]formatTime:将秒数格式化为"00:00"格式的时间显示
[*]aboutToDisappear:组件销毁时清理计时器,防止内存泄漏
四、实现长按事件处理

接下来,我们为"按住 语言"按钮添加触摸事件处理,实现长按开始录音的功能。
@Entry
@Component
struct WeChatRecorder {
// 之前的代码...

build() {
    Column() {
      Stack({ alignContent: Alignment.Center }) {
      // 暂时留空,后面会添加录音界面
      }
      .layoutWeight(1)
      
      // 底部输入栏
      Row() {
      // 录音按钮
      Text(this.isRecording ? '松开 发送' : '按住 说话')
          .fontSize(16)
          .fontColor(this.isRecording ? Color.White : '#333333')
          .backgroundColor(this.isRecording ? '#07C160' : '#F5F5F5')
          .borderRadius(4)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(40)
          .padding({ left: 10, right: 10 })
          // 添加触摸事件
          .onTouch((event) => {
            if (event.type === TouchType.Down) {
            // 按下时,记录起始位置,开始录音
            this.touchStartY = event.touches.y
            this.startRecording()
            } else if (event.type === TouchType.Move) {
            // 移动时,检测是否上滑到取消区域
            const moveDistance = this.touchStartY - event.touches.y
            this.isCancel = moveDistance > this.cancelThreshold
            } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
            // 松开或取消触摸时,结束录音
            this.stopRecording()
            }
          })
      }
      .width('100%')
      .backgroundColor(this.isRecording ? Color.Transparent : Color.White)
      .expandSafeArea()
      .padding({ left: 15, right: 15, top: 15 })
      .border({ width: { top: 1 }, color: '#E5E5E5' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#EDEDED')
    .expandSafeArea()
}
}
在这一步中,我们:

[*]为按钮文本添加了动态内容,根据录音状态显示不同文字
[*]为按钮添加了触摸事件处理,包罗按下、移动和松开/取消
[*]根据录音状态动态改变底部栏的配景致
五、实现录音界面和声颠簸画

最后,我们添加录音状态下的界面显示,包罗上滑取消提示和声颠簸画。
@Entry
@Component
struct WeChatRecorder {
// 之前的代码...

build() {
    Column() {
      // 聊天内容区域
      Stack({ alignContent: Alignment.Center }) {
      // 录音状态提示
      if (this.isRecording) {
          // 遮罩背景
          Column()
            .width('100%')
            .height('100%')
            .backgroundColor('#80000000')
            .expandSafeArea()

          Column() {
            // 上滑取消提示
            Text(this.isCancel ? '松开手指,取消发送' : '手指上滑,取消发送')
            .fontSize(14)
            .fontColor(this.isCancel ? Color.Red : '#999999')
            .backgroundColor(this.isCancel ? '#FFE9E9' : '#D1D1D1')
            .borderRadius(4)
            .padding({
                left: 10,
                right: 10,
                top: 5,
                bottom: 5
            })
            .margin({ bottom: 20 })

            // 录音界面容器
            Column() {
            // 声波动画容器
            Row() {
                ForEach(this.waveHeights, (height: number, index) => {
                  Column()
                  .width(4)
                  .height(height)
                  .backgroundColor('#7ED321')
                  .borderRadius(2)
                  .margin({ left: 3, right: 3 })
                })
            }
            .width(160)
            .height(100)
            .justifyContent(FlexAlign.Center)
            .margin({ bottom: 15 })

            // 录音时间显示
            Text(`${this.formatTime(this.recordTime)}`)
                .fontSize(16)
                .fontColor('#999999')
            }
            .width(180)
            .backgroundColor(Color.White)
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
            .padding(10)
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
      }
      }
      .layoutWeight(1)

      // 底部输入栏
      // 与之前的代码相同
    }
    // 与之前的代码相同
}
}
在这一步中,我们添加了:

[*]录音状态下的遮罩配景,利用半透明玄色配景
[*]上滑取消提示,根据 isCancel 状态显示不同内容和样式
[*]声颠簸画容器,利用 ForEach 循环遍历 waveHeights 数组创建多个柱状条
[*]录音时间显示,利用 formatTime 方法格式化时间
六、完整实现

下面是完整的实当代码:
/**
* 微信语音录制动画效果
* 实现功能:
* 1. 长按按钮: 显示录音动画
* 2. 上滑取消: 模拟取消录音
* 3. 松开发送: 模拟发送语音
*/
@Entry
@Component
struct WeChatRecorder {
// 是否正在录音
@State isRecording: boolean = false
// 是否显示取消提示(上滑状态)
@State isCancel: boolean = false
// 录音时长(秒)
@State recordTime: number = 0
// 声波高度变化数组
@State waveHeights: number[] =
// 计时器ID
private timerId: number = 0
// 波形动画计时器ID
private waveTimerId: number = 0
// 触摸起始位置
private touchStartY: number = 0
// 触摸移动阈值,超过该值显示取消提示
private readonly cancelThreshold: number = 50

/**
   * 开始录音,初始化状态及启动计时器
   */
startRecording() {
    this.isRecording = true
    this.isCancel = false
    this.recordTime = 0

    // 启动计时器,每秒更新录音时长
    this.timerId = setInterval(() => {
      this.recordTime++
    }, 1000)

    // 启动波形动画计时器,随机更新波形高度
    this.waveTimerId = setInterval(() => {
      this.updateWaveHeights()
    }, 200)
}

/**
   * 结束录音,清理计时器和状态
   */
stopRecording() {
    // 清除计时器
    if (this.timerId !== 0) {
      clearInterval(this.timerId)
      this.timerId = 0
    }

    if (this.waveTimerId !== 0) {
      clearInterval(this.waveTimerId)
      this.waveTimerId = 0
    }

    // 如果是取消状态,则显示取消提示
    if (this.isCancel) {
      console.info('录音已取消')
    } else if (this.recordTime > 0) {
      // 如果录音时长大于0,则模拟发送语音
      console.info(`发送语音,时长: ${this.recordTime}秒`)
    }

    // 重置状态
    this.isRecording = false
    this.isCancel = false
    this.recordTime = 0
}

/**
   * 更新波形高度以产生动画效果
   */
updateWaveHeights() {
    // 创建新的波形高度数组
    const newHeights = this.waveHeights.map(() => {
      // 生成20-40之间的随机高度
      return Math.floor(Math.random() * 20) + 20
    })

    this.waveHeights = newHeights
}

/**
   * 格式化时间显示,将秒转换为"00:00"格式
   */
formatTime(seconds: number): string {
    const minutes = Math.floor(seconds / 60)
    const secs = seconds % 60
    return `${minutes.toString()
      .padStart(2, '0')}:${secs.toString()
      .padStart(2, '0')}`
}

aboutToDisappear() {
    // 组件销毁时清除计时器
    if (this.timerId !== 0) {
      clearInterval(this.timerId)
      this.timerId = 0
    }

    if (this.waveTimerId !== 0) {
      clearInterval(this.waveTimerId)
      this.waveTimerId = 0
    }
}

build() {
    Column() {
      // 聊天内容区域(模拟)
      Stack({ alignContent: Alignment.Center }) {
      // 录音状态提示
      if (this.isRecording) {
          // 遮罩背景
          Column()
            .width('100%')
            .height('100%')
            .backgroundColor('#80000000')
            .expandSafeArea()

          Column() {
            // 上滑取消提示
            Text(this.isCancel ? '松开手指,取消发送' : '手指上滑,取消发送')
            .fontSize(14)
            .fontColor(this.isCancel ? Color.Red : '#999999')
            .backgroundColor(this.isCancel ? '#FFE9E9' : '#D1D1D1')
            .borderRadius(4)
            .padding({
                left: 10,
                right: 10,
                top: 5,
                bottom: 5
            })
            .margin({ bottom: 20 })

            // 录音界面容器
            Column() {
            // 声波动画容器
            Row() {
                ForEach(this.waveHeights, (height: number, index) => {
                  Column()
                  .width(4)
                  .height(height)
                  .backgroundColor('#7ED321')
                  .borderRadius(2)
                  .margin({ left: 3, right: 3 })
                })
            }
            .width(160)
            .height(100)
            .justifyContent(FlexAlign.Center)
            .margin({ bottom: 15 })

            // 录音时间显示
            Text(`${this.formatTime(this.recordTime)}`)
                .fontSize(16)
                .fontColor('#999999')
            }
            .width(180)
            .backgroundColor(Color.White)
            .borderRadius(8)
            .justifyContent(FlexAlign.Center)
            .padding(10)
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
      }
      }
      .layoutWeight(1)

      // 底部输入栏
      Row() {
      // 录音按钮
      Text(this.isRecording ? '松开 发送' : '按住 说话')
          .fontSize(16)
          .fontColor(this.isRecording ? Color.White : '#333333')
          .backgroundColor(this.isRecording ? '#07C160' : '#F5F5F5')
          .borderRadius(4)
          .textAlign(TextAlign.Center)
          .width('100%')
          .height(40)
          .padding({ left: 10, right: 10 })// 添加触摸事件
          .onTouch((event) => {
            if (event.type === TouchType.Down) {
            // 按下时,记录起始位置,开始录音
            this.touchStartY = event.touches.y
            this.startRecording()
            } else if (event.type === TouchType.Move) {
            // 移动时,检测是否上滑到取消区域
            const moveDistance = this.touchStartY - event.touches.y
            this.isCancel = moveDistance > this.cancelThreshold
            } else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
            // 松开或取消触摸时,结束录音
            this.stopRecording()
            }
          })
      }
      .width('100%')
      .backgroundColor(this.isRecording ? Color.Transparent : Color.White)
      .expandSafeArea()
      .padding({ left: 15, right: 15, top: 15 })
      .border({ width: { top: 1 }, color: '#E5E5E5' })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#EDEDED')
    .expandSafeArea()
}
}
拓展与优化

以上是根本的实现,如果想进一步优化,可以思量:

[*]真实的录音功能:利用HarmonyOS的媒体录制API实实际际录音
[*]声音波形及时变化:根据实际录音音量调整波形高度
[*]振动反馈:在录音开始、取消或发送时添加振动反馈
[*]显示已录制的语音消息:将录制好的语音添加到谈天消息列表中
[*]录音时长限制:添加最长录音时间限制(如微信的60秒)
总结

通过这个教程,我们从零开始实现了类似微信的语音录制动画效果。重要用到了以下技术:

[*]HarmonyOS的ArkUI布局系统
[*]状态管理(@State)
[*]触摸事件处理
[*]定时器和动画
[*]条件渲染
[*]组件生命周期处理
这些技术和概念不仅适用于这个特定效果,还可以应用于各种交互设计中。渴望这个教程能帮助你更好地明白HarmonyOS开辟,并创建出更加精致的应用界面!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 鸿蒙殊效教程02-微信语音录制动画