🚀 本文将带你从 0 到 1 实现一个完整可复用的语音录制与播放模块,适用于社交类 App 场景。
使用 Kotlin 封装网易云信的
AudioRecorder
,并结合MediaPlayer
实现播放、暂停、进度监听、时间显示与生命周期管理。
🧭 目录
-
[核心类 SyncRecordManager](#核心类 SyncRecordManager)
🧩 前言
在社交类应用中,语音聊天、语音评论等功能是非常常见的。
而在 Android 中实现一套录音 + 播放 + 暂停 + 恢复播放 + 时间显示的完整方案,往往涉及:
-
录音管理(启动、停止、超时、失败处理)
-
播放管理(暂停、恢复、播放完成)
-
时间更新与 UI 同步
-
生命周期内的资源释放
如果逻辑分散在各个界面中,会造成维护困难。
因此我们需要一个高内聚、低耦合的封装类。
🎯 功能设计目标
本模块需要满足以下需求:
功能 | 说明 |
---|---|
🎤 录音控制 | 点击按钮开始录音,再次点击停止录音 |
⏱️ 时间显示 | 实时显示录音/播放时间(格式:00:06) |
🎧 播放控制 | 支持播放、暂停、继续播放 |
🔁 状态切换 | 录音完成后自动进入播放状态 |
🧹 重置机制 | 播放完成或删除后重置为初始状态 |
🔒 限时录音 | 录音时长限制为 5~60 秒 |
🧩 生命周期安全 | 页面销毁时自动释放资源 |
🧱 模块结构设计
采用单一职责 + 状态驱动设计:
VoiceRecordManager ├── AudioRecorder (网易云信) ├── MediaPlayer (系统播放) ├── Handler/Timer (时间进度) ├── State Enum (状态管理) └── Callbacks (状态、进度监听)
💡 核心类 SyncRecordManager
该类负责录音、播放、暂停、继续播放的完整逻辑:
Kotlin
/**
* @class SyncRecordManager
* @desc 音频录制与播放管理类
* 封装了语音的录制、播放、暂停、继续播放、计时、状态管理等逻辑。
* 使用网易云信的 AudioRecorder 进行录制,不使用系统 MediaRecorder。
*
* 功能点:
* - 支持录音时长限制(最短5秒,最长60秒)
* - 自动回调录音/播放时间
* - 支持暂停与恢复播放
* - 状态统一管理(IDLE / RECORDING / RECORDED / PLAYING / PAUSED)
* - 播放完成、录制过短/过长、状态变化等回调通知
*/
class NimRecordManager(private val context: Context) {
/** 当前状态 */
enum class State {
IDLE, // 空闲状态
RECORDING, // 正在录音
RECORDED, // 已录音完成
PLAYING, // 正在播放
PAUSED // 播放暂停
}
/** ============= 对外回调接口 ============= */
var onStateChanged: ((State) -> Unit)? = null // 状态变化回调
var onRecordTimeUpdate: ((Int) -> Unit)? = null // 录音时长更新(秒)
var onPlayTimeUpdate: ((Int, Int) -> Unit)? = null // 播放进度更新(当前秒,总秒)
var onPlayComplete: (() -> Unit)? = null // 播放完成
var onRecordTooShortOrLong: (() -> Unit)? = null // 录音过短或过长
/** ============= 内部成员变量 ============= */
private var state: State = State.IDLE
set(value) {
field = value
onStateChanged?.invoke(value)
}
private var recorder: AudioRecorder? = null
private var mediaPlayer: MediaPlayer? = null
private var recordFilePath: String? = null
private var recordFile: File? = null
private var recordDuration = 0 // 录音时长(秒)
private val mainHandler = Handler(Looper.getMainLooper())
private var recordTimerRunnable: Runnable? = null
private var playTimerRunnable: Runnable? = null
/** 录音时长限制 */
private val maxRecordSeconds = 60
private val minRecordSeconds = 5
/** 网易云信录音回调 */
private val recordCallback = object : IAudioRecordCallback {
override fun onRecordStart(audioFile: File?, recordType: RecordType?) {
// 开始录音时的回调,可用于更新 UI
}
override fun onRecordReady() {
// 录音准备完成,可在此更新UI状态(可选)
}
override fun onRecordSuccess(audioFile: File?, audioLength: Long, recordType: RecordType?) {
// 录音成功回调
audioFile?.let { file ->
if (!file.exists()) {
onRecordFail()
return
}
// 某些机型需等待文件写入完成
val finalSize = waitForFileWrite(file)
if (finalSize == 0L) {
onRecordFail()
return
}
recordFile = file
recordFilePath = file.absolutePath
recordDuration = (audioLength / 1000).toInt()
// 校验录音时长合法性
if (recordDuration in minRecordSeconds..maxRecordSeconds) {
state = State.RECORDED
} else {
file.delete()
onRecordTooShortOrLong?.invoke()
reset(deleteFile = false)
}
} ?: run {
onRecordFail()
}
}
override fun onRecordFail() {
// 录音失败处理
stopRecordTimer()
reset(deleteFile = true)
}
override fun onRecordCancel() {
// 主动取消录音
stopRecordTimer()
reset(deleteFile = true)
}
override fun onRecordReachedMaxTime(duration: Int) {
// 达到最大录音时长,自动停止
stopRecord()
}
}
//===================== 录音相关 =====================//
/**
* toggleAction:一键控制录音与播放状态转换
* 根据当前状态自动切换对应操作
*/
fun toggleAction() {
when (state) {
State.IDLE -> startRecord()
State.RECORDING -> stopRecord()
State.RECORDED -> startPlay()
State.PLAYING -> pausePlay()
State.PAUSED -> resumePlay()
}
}
/** 开始录音 */
private fun startRecord() {
if (state != State.IDLE) return
recordDuration = 0
recordFile = null
recordFilePath = null
recorder = AudioRecorder(context, RecordType.AAC, maxRecordSeconds, recordCallback)
recorder?.startRecord()
state = State.RECORDING
startRecordTimer()
}
/** 停止录音 */
private fun stopRecord() {
if (state != State.RECORDING) return
recorder?.completeRecord(false)
stopRecordTimer()
}
/** 启动录音计时器(每秒更新) */
private fun startRecordTimer() {
recordTimerRunnable = object : Runnable {
override fun run() {
recordDuration++
onRecordTimeUpdate?.invoke(recordDuration)
if (recordDuration >= maxRecordSeconds) {
stopRecord()
} else {
mainHandler.postDelayed(this, 1000)
}
}
}
mainHandler.postDelayed(recordTimerRunnable!!, 1000)
}
/** 停止录音计时器 */
private fun stopRecordTimer() {
recordTimerRunnable?.let { mainHandler.removeCallbacks(it) }
recordTimerRunnable = null
}
//===================== 播放相关 =====================//
/** 开始播放录音文件 */
private fun startPlay() {
if (state != State.RECORDED) return
recordFilePath?.let { path ->
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(path)
prepare()
start()
setOnCompletionListener {
stopPlay()
onPlayComplete?.invoke()
}
}
state = State.PLAYING
startPlayTimer()
} catch (e: Exception) {
e.printStackTrace()
reset()
}
}
}
/** 暂停播放 */
private fun pausePlay() {
if (state != State.PLAYING) return
mediaPlayer?.pause()
state = State.PAUSED
stopPlayTimer()
}
/** 继续播放 */
private fun resumePlay() {
if (state != State.PAUSED) return
mediaPlayer?.start()
state = State.PLAYING
startPlayTimer()
}
/** 停止播放并释放资源 */
private fun stopPlay() {
mediaPlayer?.runCatching {
stop()
release()
}
mediaPlayer = null
stopPlayTimer()
state = State.RECORDED
}
/** 启动播放进度计时(每500ms更新一次UI) */
private fun startPlayTimer() {
playTimerRunnable = object : Runnable {
override fun run() {
mediaPlayer?.let {
if (it.isPlaying) {
val posSec = it.currentPosition / 1000
val durSec = it.duration / 1000
onPlayTimeUpdate?.invoke(posSec, durSec)
mainHandler.postDelayed(this, 500)
}
}
}
}
mainHandler.post(playTimerRunnable!!)
}
/** 停止播放计时器 */
private fun stopPlayTimer() {
playTimerRunnable?.let { mainHandler.removeCallbacks(it) }
playTimerRunnable = null
}
//===================== 重置 & 工具方法 =====================//
/**
* 重置状态并释放资源
* @param deleteFile 是否删除录音文件
*/
fun reset(deleteFile: Boolean = true) {
stopRecordTimer()
stopPlayTimer()
recorder?.completeRecord(true)
recorder = null
mediaPlayer?.runCatching {
stop()
release()
}
mediaPlayer = null
if (deleteFile) {
recordFilePath?.let { File(it).delete() }
recordFile = null
recordFilePath = null
}
recordDuration = 0
state = State.IDLE
}
/**
* 等待文件写入完成,确保录音文件可用
*/
private fun waitForFileWrite(file: File, maxWaitMs: Long = 300): Long {
val start = System.currentTimeMillis()
var size = file.length()
while (size == 0L && System.currentTimeMillis() - start < maxWaitMs) {
Thread.sleep(20)
size = file.length()
}
return size
}
/** ============= 对外工具方法 ============= */
fun getFilePath(): String? = recordFilePath
fun getFile(): File? = recordFile
fun getDuration(): Int = recordDuration
}
🧩 使用方式
Kotlin
private val voiceManager by lazy { VoiceRecordManager(requireContext()) }
binding.btnVoice.setOnClickListener {
when (voiceManager.state) {
VoiceRecordManager.State.IDLE -> {
// 开始录音
voiceManager.startRecord()
}
VoiceRecordManager.State.RECORDING -> {
// 停止录音
voiceManager.stopRecord()
}
VoiceRecordManager.State.RECORDED -> {
// 播放录音
voiceManager.startPlay()
}
VoiceRecordManager.State.PLAYING -> {
// 暂停播放
voiceManager.pausePlay()
}
VoiceRecordManager.State.PAUSED -> {
// 继续播放
voiceManager.resumePlay()
}
}
}
🧠 实现思路详解
状态管理
定义 State
枚举,保证流程转换清晰:
Kotlin
enum class State { IDLE, RECORDING, RECORDED, PLAYING, PAUSED }
录音定时器
通过 Handler
定时更新录音时间:
Kotlin
private fun startRecordTimer() {
mainHandler.postDelayed({
recordDuration++
onRecordTimeUpdate?.invoke(recordDuration)
}, 1000)
}
播放控制与进度监听
MediaPlayer
播放状态定期回调进度:
Kotlin
val pos = it.currentPosition / 1000
val dur = it.duration / 1000
onPlayTimeUpdate?.invoke(pos, dur)
资源释放与重置机制
在异常、取消或销毁时释放资源:
Kotlin
fun reset(deleteFile: Boolean = true) {
recorder?.completeRecord(true)
mediaPlayer?.release()
state = State.IDLE
}
⚙️ 性能优化与注意事项
-
主线程安全:录音与播放逻辑尽量避免阻塞主线程。
-
播放进度防抖:更新频率建议控制在 500ms。
-
录音文件写入延迟:部分设备需等待文件完全写入。
-
录音时间限制:5s 以下、60s 以上的录音均视为无效。
-
生命周期安全 :可使用
DefaultLifecycleObserver
自动释放资源。
🏁 结语
通过本次封装,我们实现了一个:
-
支持录音/播放/暂停/继续播放;
-
带时间更新与状态回调;
-
支持时长限制与生命周期安全;
的完整语音管理模块。
这种封装思路可轻松拓展至视频录制、语音评论、语音私聊等功能场景,极大提升代码复用率与项目可维护性。