兩個以上的 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() 回呼。
雖然自動降低音量是音樂和影片播放應用程式可接受的行為,但在播放語音內容 (例如有聲書應用程式) 時,這種行為並不實用。在這種情況下,應用程式應改為暫停播放。
如果希望應用程式在收到降低音量要求時暫停播放,而不是降低音量,請建立 OnAudioFocusChangeListener,並使用 onAudioFocusChange() 回呼方法實作所需的暫停/繼續行為。呼叫 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。這個事件監聽器應在擁有媒體工作階段的同一個活動或服務中建立。它會實作回呼 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 中的 onAudioFocusChange() 方法,該 AudioFocusChangeListener 是應用程式呼叫 requestAudioFocus() 時指定的。
傳遞至 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() 回呼中呼叫這個方法。