兩個以上的 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()
通知您的應用程式。
為了處理延遲獲得焦點的情形,您必須使用 onAudioFocusChange()
回呼方法建立 OnAudioFocusChangeListener
,該方法會實作所需行為,並透過呼叫 setOnAudioFocusChangeListener()
註冊事件監聽器。
Android 7.1 以下版本中的音訊焦點
呼叫 requestAudioFocus()
時,您必須指定時間長度提示,目前正在持有焦點並播放的其他應用程式可能會遵循這個提示:
- 如要在可預測的未來播放音訊 (例如播放音樂),且希望先前的音訊焦點持有者停止播放,請要求永久音訊焦點 (
AUDIOFOCUS_GAIN
)。 - 如要只播放音訊一段短時間,並希望先前的持有者暫停播放,請要求暫時焦點 (
AUDIOFOCUS_GAIN_TRANSIENT
)。 - 請使用降低音量 (
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
) 要求暫時焦點,表示您只想播放音訊一段短時間,且如果先前的焦點擁有者降低音訊輸出,您也能繼續播放。兩個音訊輸出都會混合至音訊串流。對於間歇性使用音訊串流的應用程式 (例如提供語音行車路線),使用迴避功能特別合適。
requestAudioFocus()
方法也需要 AudioManager.OnAudioFocusChangeListener
。這個事件監聽器應在擁有媒體工作階段的相同活動或服務中建立。它會實作回呼 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
回呼,因此應立即暫停播放。如要重新開始播放,使用者必須採取明確的動作,例如在通知或應用程式 UI 中按下播放傳輸控制項。
下列程式碼片段示範如何實作 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()
回呼中呼叫這個方法。