兩個以上的 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 之前的 durationHint 呼叫中使用的 requestAudioFocus() 相同: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
)。 - 使用 ducking (
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
) 要求暫時性焦點,表示您預期只會短暫播放音訊,且先前的焦點擁有者可以繼續播放音訊 (如果「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()
方法的呼叫,而您在應用程式呼叫 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)
。舉例來說,請在 Callback 的 onPlay()
、onSkipToNext()
等中呼叫 removeCallbacks()
。您也應該在服務的 onDestroy()
回呼中呼叫這個方法,以便清除服務使用的資源。