两个或多个 Android 应用可以同时将音频播放到同一输出流,系统会将所有音频混合在一起。虽然这在技术上非常惊人,但可能会令用户非常头疼。为了避免每个音乐应用同时播放,Android 引入了“音频焦点”的概念。一次只能有一个应用获得音频焦点。
当您的应用需要输出音频时,它需要请求获得音频焦点,在获得焦点时,它可以发出声音。不过,获得音频焦点后,您可能无法将焦点一直保留到播放完成。其他应用可以请求焦点,从而抢占您持有的音频焦点。如果发生这种情况,您的应用应暂停播放或调低音量,以便用户能够更轻松地听到新的音频源。
在 Android 12(API 级别 31)之前,音频焦点不由系统管理。因此,虽然我们鼓励应用开发者遵守音频焦点准则,但如果应用在搭载 Android 11(API 级别 30)或更低版本的设备上丢失音频焦点后仍继续大声播放,系统将无法阻止此类播放。但是,此应用行为会导致糟糕的用户体验,并且常常会导致用户卸载出现异常的应用。
设计良好的音频应用应根据以下一般准则管理音频焦点:
在开始播放前调用
requestAudioFocus()
,并验证调用是否返回AUDIOFOCUS_REQUEST_GRANTED
。在媒体会话的onPlay()
回调中调用requestAudioFocus()
。在其他应用获得音频焦点时,停止或暂停播放,或降低音量(即降低音量)。
播放停止时(例如,应用没有要播放的内容时),放弃音频焦点。如果用户暂停播放但可能稍后继续播放,您的应用无需放弃音频焦点。
使用
AudioAttributes
描述您的应用正在播放的音频类型。例如,对于播放语音的应用,请指定CONTENT_TYPE_SPEECH
。
音频焦点的处理方式因运行的 Android 版本而异:
- Android 12(API 级别 31)或更高版本
- 音频焦点由系统管理。当其他应用请求音频焦点时,系统会强制使某个应用的音频播放淡出。收到来电时,系统也会将音频播放静音。
- Android 8.0(API 级别 26)到 Android 11(API 级别 30)
- 音频焦点不受系统管理,但包含从 Android 8.0(API 级别 26)开始引入的一些变更。
- Android 7.1(API 级别 25)及更低版本
- 音频焦点不受系统管理,应用使用
requestAudioFocus()
和abandonAudioFocus()
管理音频焦点。
Android 12 及更高版本中的音频焦点
使用音频焦点的媒体或游戏应用在失去焦点后不应播放音频。在 Android 12(API 级别 31)及更高版本中,系统会强制执行此行为。当应用请求音频焦点,而另一个应用具有焦点并正在播放时,系统会强制正在播放的应用淡出。添加淡出后,当用户从一个应用切换到另一个应用时,可以实现更顺畅的过渡。
当满足以下条件时,就会出现这种淡出行为:
第一个当前正在播放的应用满足以下所有条件:
- 应用具有
AudioAttributes.USAGE_MEDIA
或AudioAttributes.USAGE_GAME
用法属性。 - 应用使用
AudioManager.AUDIOFOCUS_GAIN
成功请求了音频焦点。 - 应用当前未播放内容类型为
AudioAttributes.CONTENT_TYPE_SPEECH
的音频。
- 应用具有
第二个应用使用
AudioManager.AUDIOFOCUS_GAIN
请求音频焦点。
当满足这些条件时,音频系统会淡出第一个应用。在淡出结束时,系统会通知第一个应用失去焦点。应用的播放器将保持静音,直到应用再次请求获得音频焦点。
现有的音频焦点行为
您还应注意下面这些涉及切换音频焦点的其他情况。
自动降低音量
自动降低音量功能是在 Android 8.0(API 级别 26)中引入的。该功能会暂时降低一个应用的音量,以便另一个应用能被清晰地听到。
通过让系统实现降低其他应用音量功能,您无需在应用中实现降低其他应用音量功能。
当音频通知从正在播放音频的应用夺取焦点时,也会出现自动降低音量的行为。通知播放的开头与降低音量坡道的结尾同步。
当满足以下条件时,就会出现自动降低音量的行为:
当前播放的第一个应用满足以下所有条件:
- 应用成功请求了任何类型的焦点获取的音频焦点。
- 应用当前未播放内容类型为
AudioAttributes.CONTENT_TYPE_SPEECH
的音频。 - 应用未设置
AudioFocusRequest.Builder.setWillPauseWhenDucked(true)
。
第二个应用使用
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
请求音频焦点。
当满足这些条件时,音频系统会闪避第一个应用的所有活跃播放器,而第二个应用获得焦点。当第二个应用放弃焦点时,它会取消降低音量。第一个应用在失去焦点时不会收到通知,因此无需执行任何操作。
请注意,当用户收听语音内容时,系统不会执行自动降低音量的功能,因为用户可能会错过某些节目。例如,行车路线的语音导航不会闪避。
将当前来电的音频播放设为静音
某些应用无法正常运行,在通话期间会继续播放音频。 这种情况迫使用户找到违规应用并将其静音,或退出违规应用,以便听到通话内容。为了防止出现这种情况,系统可以在有来电时将来自其他应用的音频静音。当有来电且应用满足以下条件时,系统就会调用此方法:
- 应用具有
AudioAttributes.USAGE_MEDIA
或AudioAttributes.USAGE_GAME
用法属性。 - 应用成功请求了音频焦点(任何焦点增益),并正在播放音频。
如果某个应用在通话过程中继续播放,其播放会静音,直到通话结束。不过,如果应用在通话期间开始播放,该播放器不会因用户是有意开始播放的假设而静音。
Android 8.0 至 Android 11 中的音频焦点
从 Android 8.0(API 级别 26)开始,调用 requestAudioFocus()
时,必须提供 AudioFocusRequest
参数。AudioFocusRequest
包含有关应用的音频上下文和功能的信息。系统会使用这些信息自动管理音频焦点的获取和失去。如需释放音频焦点,请调用 abandonAudioFocusRequest()
方法,该方法也接受 AudioFocusRequest
作为其参数。请求和放弃焦点时使用相同的 AudioFocusRequest
实例。
如需创建 AudioFocusRequest
,请使用 AudioFocusRequest.Builder
。由于焦点请求必须始终指定请求的类型,因此该类型会包含在构建器的构造函数中。使用构建器的方法设置请求的其他字段。
FocusGain
字段为必需字段;所有其他字段均为可选字段。
方法 | 备注 |
---|---|
setFocusGain()
|
每个请求中都必须包含此字段。它的值与 Android 8.0 之前的 requestAudioFocus() 调用中使用的 durationHint 值相同:AUDIOFOCUS_GAIN 、AUDIOFOCUS_GAIN_TRANSIENT 、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 或 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 。 |
setAudioAttributes()
|
AudioAttributes 描述的是应用的用例。系统会在应用获得和失去音频焦点时查看这些用例。属性会取代音频流类型的概念。在 Android 8.0(API 级别 26)及更高版本中,废弃了除音量控制以外的任何操作的流类型。在焦点请求中使用您在音频播放器中使用的相同属性(如下表后面的示例所示)。
先使用
如果未指定,则 |
setWillPauseWhenDucked()
|
当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 请求获得焦点时,获得焦点的应用通常不会收到 onAudioFocusChange() 回调,因为系统可以自行降低音量。如果您需要暂停播放而不是降低音量,请调用 setWillPauseWhenDucked(true) 并创建并设置 OnAudioFocusChangeListener ,如自动降低音量中所述。 |
setAcceptsDelayedFocusGain()
|
当焦点被其他应用锁定时,对音频焦点的请求可能会失败。此方法可实现延迟获取焦点,即在焦点可用时异步获取焦点。
请注意,只有当您在音频请求中也指定了 |
setOnAudioFocusChangeListener()
|
仅当您还在请求中指定 willPauseWhenDucked(true) 或 setAcceptsDelayedFocusGain(true) 时,才需要 OnAudioFocusChangeListener 。
设置监听器的方法有两种:一种带有处理程序参数,另一种则不带有处理程序参数。处理程序是运行监听器的线程。如果您未指定处理程序,系统会使用与主 |
以下示例展示了如何使用 AudioFocusRequest.Builder
构建 AudioFocusRequest
并请求和放弃音频焦点:
Kotlin
// initializing variables for audio focus and playback management audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { setAudioAttributes(AudioAttributes.Builder().run { setUsage(AudioAttributes.USAGE_GAME) setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) build() }) setAcceptsDelayedFocusGain(true) setOnAudioFocusChangeListener(afChangeListener, handler) build() } val focusLock = Any() var playbackDelayed = false var playbackNowAuthorized = false // requesting audio focus and processing the response val res = audioManager.requestAudioFocus(focusRequest) synchronized(focusLock) { playbackNowAuthorized = when (res) { AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> { playbackNow() true } AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> { playbackDelayed = true false } else -> false } } // implementing OnAudioFocusChangeListener to react to focus changes override fun onAudioFocusChange(focusChange: Int) { when (focusChange) { AudioManager.AUDIOFOCUS_GAIN -> if (playbackDelayed || resumeOnFocusGain) { synchronized(focusLock) { playbackDelayed = false resumeOnFocusGain = false } playbackNow() } AudioManager.AUDIOFOCUS_LOSS -> { synchronized(focusLock) { resumeOnFocusGain = false playbackDelayed = false } pausePlayback() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { synchronized(focusLock) { // only resume if playback is being interrupted resumeOnFocusGain = isPlaying() playbackDelayed = false } pausePlayback() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { // ... pausing or ducking depends on your app } } }
Java
// initializing variables for audio focus and playback management audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE); playbackAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_GAME) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build(); focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) .setAudioAttributes(playbackAttributes) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(afChangeListener, handler) .build(); final Object focusLock = new Object(); boolean playbackDelayed = false; boolean playbackNowAuthorized = false; // requesting audio focus and processing the response int res = audioManager.requestAudioFocus(focusRequest); synchronized(focusLock) { if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { playbackNowAuthorized = false; } else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { playbackNowAuthorized = true; playbackNow(); } else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) { playbackDelayed = true; playbackNowAuthorized = false; } } // implementing OnAudioFocusChangeListener to react to focus changes @Override public void onAudioFocusChange(int focusChange) { switch (focusChange) { case AudioManager.AUDIOFOCUS_GAIN: if (playbackDelayed || resumeOnFocusGain) { synchronized(focusLock) { playbackDelayed = false; resumeOnFocusGain = false; } playbackNow(); } break; case AudioManager.AUDIOFOCUS_LOSS: synchronized(focusLock) { resumeOnFocusGain = false; playbackDelayed = false; } pausePlayback(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: synchronized(focusLock) { // only resume if playback is being interrupted resumeOnFocusGain = isPlaying(); playbackDelayed = false; } pausePlayback(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: // ... pausing or ducking depends on your app break; } } }
自动降低音量
在 Android 8.0(API 级别 26)中,当其他应用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
请求焦点时,系统可以在不调用应用的 onAudioFocusChange()
回调的情况下降低和恢复音量。
虽然音乐和视频播放应用接受自动降低音量的行为,但在播放语音内容时(例如在有声读物应用中)就没什么用了。在这种情况下,应用应改为暂停播放。
如果您希望应用在被要求降低音量时暂停,请创建包含 onAudioFocusChange()
回调方法的 OnAudioFocusChangeListener
,该回调方法可以实现所需的暂停/恢复行为。调用 setOnAudioFocusChangeListener()
以注册监听器,然后调用 setWillPauseWhenDucked(true)
以告知系统使用您的回调,而不是执行自动降低音量。
延迟获取焦点
有时,系统无法批准音频焦点请求,因为焦点已被其他应用“锁定”,例如在通话期间。在这种情况下,requestAudioFocus()
会返回 AUDIOFOCUS_REQUEST_FAILED
。在这种情况下,您的应用不应继续播放音频,因为它没有获得焦点。
方法 setAcceptsDelayedFocusGain(true)
,让您的应用异步处理焦点请求。设置此标志后,在焦点锁定时发出的请求会返回 AUDIOFOCUS_REQUEST_DELAYED
。当锁定音频焦点的情况不再存在时(例如当通话结束时),系统会批准待处理的焦点请求,并调用 onAudioFocusChange()
来通知您的应用。
为了处理延迟获取焦点,您必须创建一个 OnAudioFocusChangeListener
,其中包含实现所需行为的 onAudioFocusChange()
回调方法,并通过调用 setOnAudioFocusChangeListener()
注册监听器。
Android 7.1 及更低版本中的音频焦点
调用 requestAudioFocus()
时,您必须指定时长提示,当前持有焦点并正在播放的另一个应用可能会遵循该提示:
- 如果您计划在可预见的未来(例如,播放音乐时)播放音频,并且希望上一个音频焦点持有者停止播放,则需要请求永久音频焦点 (
AUDIOFOCUS_GAIN
)。 - 如果您只打算播放短时间的音频,并且希望上一个播放者暂停播放,可以请求暂时性焦点 (
AUDIOFOCUS_GAIN_TRANSIENT
)。 - 通过闪避
(
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
) 请求暂时性焦点,以表明您希望仅播放短时间的音频,并且之前的焦点所有者可以在“降低”(降低)音频输出的情况下继续播放。这两种音频输出都会混合到音频流中。降低音量功能尤其适用于间歇性使用音频流的应用,例如有声的行车路线。
requestAudioFocus()
方法还需要一个 AudioManager.OnAudioFocusChangeListener
。您应在媒体会话所在的 activity 或服务中创建此监听器。它会实现您的应用在其他应用获取或放弃音频焦点时收到的回调 onAudioFocusChange()
。
以下代码段会请求对流 STREAM_MUSIC
的永久音频焦点,并注册 OnAudioFocusChangeListener
以处理音频焦点的后续更改。(有关更改监听器的说明,请参阅响应音频焦点更改。)
Kotlin
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager lateinit var afChangeListener AudioManager.OnAudioFocusChangeListener ... // Request audio focus for playback val result: Int = audioManager.requestAudioFocus( afChangeListener, // Use the music stream. AudioManager.STREAM_MUSIC, // Request permanent focus. AudioManager.AUDIOFOCUS_GAIN ) if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // Start playback }
Java
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); AudioManager.OnAudioFocusChangeListener afChangeListener; ... // Request audio focus for playback int result = audioManager.requestAudioFocus(afChangeListener, // Use the music stream. AudioManager.STREAM_MUSIC, // Request permanent focus. AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { // Start playback }
播放完毕后,调用 abandonAudioFocus()
。
Kotlin
audioManager.abandonAudioFocus(afChangeListener)
Java
// Abandon audio focus when playback complete audioManager.abandonAudioFocus(afChangeListener);
这会通知系统您不再需要焦点,并取消注册关联的 OnAudioFocusChangeListener
。如果您请求了暂时性焦点,这将通知已暂停或降低音量的应用可以继续播放或恢复其音量。
响应音频焦点更改
应用在获得音频焦点时,必须能够在其他应用为自己请求音频焦点时释放该焦点。发生这种情况时,您的应用会收到对 AudioFocusChangeListener
(您在应用调用 requestAudioFocus()
时指定的)中的 onAudioFocusChange()
方法的调用。
传递给 onAudioFocusChange()
的 focusChange
参数表示正在发生的更改类型。它对应于获取焦点的应用所使用的时长提示。您的应用应做出适当的响应。
- 暂时性失去焦点
-
如果焦点变化是暂时性的(
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
或AUDIOFOCUS_LOSS_TRANSIENT
),您的应用应降低音量(如果您不依赖于自动降低音量)或暂停播放,但以其他方式保持相同状态。在暂时性失去音频焦点期间,您应继续监控音频焦点的变化,并准备好在重新获得焦点时恢复正常播放。当阻塞应用放弃焦点时,您会收到回调 (
AUDIOFOCUS_GAIN
)。此时,您可以将音量恢复到正常水平或重新开始播放。 - 永久性失去焦点
-
如果音频焦点丢失是永久性 (
AUDIOFOCUS_LOSS
),则说明其他应用正在播放音频。您的应用应立即暂停播放,因为它不会收到AUDIOFOCUS_GAIN
回调。如需重新开始播放,用户必须执行明确的操作,例如按通知或应用界面中的播放传输控件。
以下代码段演示了如何实现 OnAudioFocusChangeListener
及其 onAudioFocusChange()
回调。请注意,它使用了 Handler
来延迟在音频焦点永久失去时停止回调。
Kotlin
private val handler = Handler() private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> { // Permanent loss of audio focus // Pause playback immediately mediaController.transportControls.pause() // Wait 30 seconds before stopping playback handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { // Pause playback } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { // Lower the volume, keep playing } AudioManager.AUDIOFOCUS_GAIN -> { // Your app has been granted audio focus again // Raise volume to normal, restart playback if necessary } } }
Java
private Handler handler = new Handler(); AudioManager.OnAudioFocusChangeListener afChangeListener = new AudioManager.OnAudioFocusChangeListener() { public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { // Permanent loss of audio focus // Pause playback immediately mediaController.getTransportControls().pause(); // Wait 30 seconds before stopping playback handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)); } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { // Pause playback } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // Lower the volume, keep playing } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Your app has been granted audio focus again // Raise volume to normal, restart playback if necessary } } };
处理程序使用如下所示的 Runnable
:
Kotlin
private var delayedStopRunnable = Runnable { mediaController.transportControls.stop() }
Java
private Runnable delayedStopRunnable = new Runnable() { @Override public void run() { getMediaController().getTransportControls().stop(); } };
为了确保在用户重新开始播放时不会触发延迟停止,请调用 mHandler.removeCallbacks(mDelayedStopRunnable)
来响应任何状态更改。例如,在回调的 onPlay()
、onSkipToNext()
等中调用 removeCallbacks()
。在清理服务使用的资源时,您还应在服务的 onDestroy()
回调中调用此方法。