Android全局播放音频工具类(Kotlin)及在Compose中使用

前言

在 Android 中编写一个全局可调用的播放音频的单例工具类,可以借助 object 关键字实现单例,并封装 MediaPlayer 的常用操作(如播放、暂停、停止等)。

工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import android.content.Context
import android.media.MediaPlayer
import android.net.Uri
import androidx.annotation.RawRes
import java.io.IOException

object ZAudioPlayer {
private var mediaPlayer: MediaPlayer? = null

/**
* 播放 raw 资源中的音频
* @param context 上下文(建议传 Application Context)
* @param resId raw 资源 ID,例如 R.raw.sound
*/
fun playRaw(context: Context, @RawRes resId: Int) {
reset()
try {
mediaPlayer = MediaPlayer.create(context.applicationContext, resId)
mediaPlayer?.setOnCompletionListener { reset() }
mediaPlayer?.start()
} catch (e: Exception) {
e.printStackTrace()
reset()
}
}

/**
* 播放 assets 目录下的音频文件
* @param context 上下文
* @param assetFileName assets 目录下的文件名,如 "sound.mp3"
*/
fun playAsset(context: Context, assetFileName: String) {
reset()
try {
val assetFd = context.assets.openFd(assetFileName)
mediaPlayer = MediaPlayer().apply {
setDataSource(assetFd.fileDescriptor, assetFd.startOffset, assetFd.length)
prepare()
setOnCompletionListener {
assetFd.close()
reset()
}
start()
}
} catch (e: IOException) {
e.printStackTrace()
reset()
}
}

/**
* 播放网络或本地 URI 音频
* @param context 上下文
* @param uri 音频 URI
*/
fun playUri(context: Context, uri: Uri) {
reset()
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(context, uri)
prepareAsync()
setOnPreparedListener { start() }
setOnCompletionListener { reset() }
}
} catch (e: Exception) {
e.printStackTrace()
reset()
}
}

/**
* 播放网络URL音频
* @param context 上下文
* @param url 音频 URL
*/
fun playUrl(context: Context, url: String) {
stop()
val uri = Uri.parse(url)
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(context, uri)
prepareAsync()
setOnPreparedListener { start() }
setOnCompletionListener { reset() }
}
} catch (e: Exception) {
e.printStackTrace()
reset()
}
}

/**
* 暂停播放
*/
fun pause() {
mediaPlayer?.takeIf { it.isPlaying }?.pause()
}

/**
* 停止并释放资源
*/
fun stop() {
reset()
}

/**
* 重置播放器
*/
private fun reset() {
mediaPlayer?.apply {
if (isPlaying) stop()
release()
}
mediaPlayer = null
}

/**
* 是否正在播放
*/
fun isPlaying(): Boolean = mediaPlayer?.isPlaying == true
}

使用示例

播放

播放网络音频

1
2
val uri = Uri.parse("https://example.com/sound.mp3")
ZAudioPlayer.playUri(applicationContext, uri)

或者

1
2
val url = "https://example.com/sound.mp3"
ZAudioPlayer.playUrl(applicationContext, url)

播放 raw 资源

1
ZAudioPlayer.playRaw(applicationContext, R.raw.click_sound)

播放 assets 文件

1
ZAudioPlayer.playAsset(applicationContext, "notification.mp3")

暂停 / 停止

1
2
ZAudioPlayer.pause()
ZAudioPlayer.stop()

Compose中使用

viewModel中定义

1
2
3
fun playAudio(applicationContext: android.content.Context, url: String) {
ZAudioPlayer.playUrl(applicationContext, url)
}

Compose中调用

获取上下文

1
2
val context = LocalContext.current
val appContext = context.applicationContext

调用

1
vm.playAudio(appContext, url)

页面销毁时停止播放

1
2
3
4
5
DisposableEffect(Unit) {
onDispose {
ZAudioPlayer.stop()
}
}

全局appContext

定义

1
2
3
val LocalAppContext = compositionLocalOf<Context> {
error("AppContext not provided")
}

提供

1
2
3
4
5
val context = LocalContext.current
val appContext = context.applicationContext
CompositionLocalProvider(LocalAppContext provides appContext) {

}

使用

1
val appContext = LocalAppContext.current

注意事项

上下文建议使用 applicationContext,防止内存泄漏。

如果需要同时播放多个音频,此单例不适用(因为只维护一个 MediaPlayer 实例)。

对于短音效(如按钮点击声),建议使用 SoundPool;对于背景音乐或长音频,MediaPlayer 更合适。

网络音频需添加网络权限:

1
<uses-permission android:name="android.permission.INTERNET" />