管理音訊焦點

兩個以上的 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) 以上版本中,系統會強制執行這項行為。當應用程式要求音訊焦點,而另一個應用程式已取得焦點並正在播放時,系統會強制播放中的應用程式淡出。新增淡出效果後,從一個應用程式切換到另一個應用程式時,轉換過程會更順暢。

當符合下列條件時,系統就會執行這項淡出行為:

  1. 第一個目前正在播放的應用程式符合下列所有條件:

  2. 第二個應用程式使用 AudioManager.AUDIOFOCUS_GAIN 要求音訊焦點。

當這些條件皆符合時,音訊系統會淡出第一個應用程式。淡出結束時,系統會通知第一個應用程式失去焦點。應用程式會繼續將播放器靜音,直到應用程式再次要求音訊焦點為止。

現有的音訊焦點行為

您也應留意其他涉及音訊焦點切換的情況。

自動降低音量

自動降低背景音量功能 (暫時降低某個應用程式的音量,以便清楚聽見另一個應用程式的音訊) 已在 Android 8.0 (API 級別 26) 中推出。

只要讓系統實作靜音功能,您就不必在應用程式中實作靜音功能。

當音訊通知從播放中應用程式取得焦點時,也會發生自動調低音量情形。通知播放的開始時間會與調低音量的結束時間同步。

系統會在符合下列條件時自動降低音量:

  1. 第一個目前正在播放的應用程式符合下列所有條件:

  2. 第二個應用程式使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 要求音訊焦點。

在符合這些條件時,當第二個應用程式獲得焦點時,音訊系統會將第一個應用程式的所有有效播放器靜音。當第二個應用程式放棄焦點時,就會取消鎖定。第一個應用程式在失去焦點時不會收到通知,因此不需要採取任何行動。

請注意,使用者在聆聽語音內容時,系統不會執行自動調低音量功能,因為使用者可能會錯過部分節目。舉例來說,開車路線的語音導航不會降低音量。

將來電時的音訊播放設為靜音

部分應用程式無法正常運作,並在通話期間繼續播放音訊。在這種情況下,使用者必須找到並靜音或關閉有問題的應用程式,才能聽到通話內容。為避免這種情況發生,系統會在有來電時將其他應用程式的音訊靜音。當系統接到來電,且應用程式符合下列條件時,系統就會叫用這項功能:

  • 應用程式具有 AudioAttributes.USAGE_MEDIAAudioAttributes.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_GAINAUDIOFOCUS_GAIN_TRANSIENTAUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCKAUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
setAudioAttributes() AudioAttributes 會說明應用程式的用途。當應用程式獲得和失去音訊焦點時,系統會查看這些值。屬性會取代串流類型的概念。在 Android 8.0 (API 級別 26) 以上版本中,除了音量控制之外,所有作業的串流類型皆已淘汰。在焦點要求中使用與音訊播放器相同的屬性 (如本表後方的範例所示)。

請先使用 AudioAttributes.Builder 指定屬性,然後使用這個方法將屬性指派給要求。

如未指定,AudioAttributes 預設為 AudioAttributes.USAGE_MEDIA

setWillPauseWhenDucked() 當其他應用程式使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 要求焦點時,具有焦點的應用程式通常不會收到 onAudioFocusChange() 回呼,因為系統可以自行執行遮蔽動作。如果您需要暫停播放,而非降低音量,請呼叫 setWillPauseWhenDucked(true),並建立及設定 OnAudioFocusChangeListener,如「自動降低音量」一節所述。
setAcceptsDelayedFocusGain() 如果焦點遭其他應用程式鎖定,音訊焦點要求可能會失敗。這個方法可啟用延遲焦點增益:在焦點可用時異步取得焦點的功能。

請注意,只有在您在音訊要求中指定 AudioManager.OnAudioFocusChangeListener 時,延遲焦點增益功能才會運作,因為應用程式需要收到回呼,才能得知焦點已授予。

setOnAudioFocusChangeListener() 只有在要求中也指定 willPauseWhenDucked(true)setAcceptsDelayedFocusGain(true) 時,才需要 OnAudioFocusChangeListener

設定事件監聽器的方法有兩種:一種是使用處理常數引數,另一種則是沒有處理常數引數。處理常式是事件監聽器執行的執行緒。如果您未指定處理常式,系統會使用與主 Looper 相關聯的處理常式。

以下範例說明如何使用 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_DUCKAUDIOFOCUS_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() 回呼中呼叫這個方法。