Android 中音频焦点的使用场景及代码示例
- 一、音频焦点简介
- 二、使用场景
- 三、版本兼容性处理
- 四、音频焦点与 MediaSession 结合使用
- 五、音频焦点在前台服务中的使用
- 六、ExoPlayer 中的音频焦点处理
- 七、音频焦点与蓝牙设备交互
- 八、音频焦点请求失败的处理策略
- 九、最佳实践与注意事项
一、音频焦点简介
在 Android 系统中,音频焦点(Audio Focus)是一种机制,用于管理多个应用程序同时播放音频时的冲突。当一个应用程序请求音频焦点并获得它时,其他应用程序在播放音频时需要做出相应的调整,以避免多个音频同时播放造成混乱。

二、使用场景
(一)音乐播放器
- 当用户正在使用音乐播放器收听歌曲时,若有来电,音乐播放器应暂停播放,以让电话铃声能够清晰地被听到。当电话结束后,音乐播放器可以根据情况恢复播放。
- 若用户在听音乐的过程中打开了另一个音乐类应用,此时正在播放音乐的应用应该暂停或降低音量,以避免两个音乐同时播放。
(二)语音导航应用
- 在用户使用语音导航的同时,如果有音乐播放,导航的语音提示应该能够优先播放,确保用户能够清楚地听到导航指令。
- 当导航语音提示结束后,音乐可以恢复正常播放。
(三)社交类应用的语音消息
- 当用户在收听社交类应用的语音消息时,若有其他音频正在播放,应该暂停或降低其他音频的音量,以便用户能够听清语音消息。
- 语音消息播放完毕后,其他音频可以恢复播放。
三、版本兼容性处理
(一)Android 8.0 及以上版本
在 Android 8.0(API 26)及以上版本中,音频焦点的请求方式发生了变化,推荐使用 AudioFocusRequest 来构建请求:
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
public class AudioFocusHelper {
private AudioManager audioManager;
private AudioFocusRequest focusRequest;
private OnAudioFocusChangeListener focusChangeListener;
public interface OnAudioFocusChangeListener {
void onAudioFocusChange(int focusChange);
}
public AudioFocusHelper(Context context, OnAudioFocusChangeListener listener) {
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.focusChangeListener = listener;
}
public boolean requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChangeListener::onAudioFocusChange)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
} else {
int result = audioManager.requestAudioFocus(focusChangeListener::onAudioFocusChange,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
}
public void abandonAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (focusRequest != null) {
audioManager.abandonAudioFocusRequest(focusRequest);
}
} else {
audioManager.abandonAudioFocus(focusChangeListener::onAudioFocusChange);
}
}
}
四、音频焦点与 MediaSession 结合使用
在现代 Android 应用中,推荐将音频焦点与 MediaSession 结合使用,以提供更好的媒体控制体验。以下是使用 AndroidX 库的更新版本:
import android.content.Context;
import android.media.AudioManager;
import androidx.media.session.MediaSessionCompat;
import androidx.media.app.NotificationCompat;
import androidx.media.session.PlaybackStateCompat;
public class MediaPlaybackManager {
private MediaSessionCompat mediaSession;
private AudioFocusHelper audioFocusHelper;
private boolean playOnAudioFocus = false;
private Context context;
public MediaPlaybackManager(Context context) {
this.context = context;
audioFocusHelper = new AudioFocusHelper(context, this::onAudioFocusChange);
mediaSession = new MediaSessionCompat(context, "MediaPlaybackManager");
setupMediaSession();
}
private void setupMediaSession() {
mediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
if (audioFocusHelper.requestAudioFocus()) {
mediaSession.setActive(true);
// 开始播放
startPlayback();
} else {
playOnAudioFocus = true;
}
}
@Override
public void onPause() {
// 暂停播放
pausePlayback();
}
@Override
public void onStop() {
audioFocusHelper.abandonAudioFocus();
mediaSession.setActive(false);
// 停止播放
stopPlayback();
}
});
// 设置播放状态
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PAUSE |
PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_STOP)
.setState(PlaybackStateCompat.STATE_NONE, 0, 1.0f)
.build();
mediaSession.setPlaybackState(state);
}
private void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
if (playOnAudioFocus) {
playOnAudioFocus = false;
startPlayback();
}
// 恢复音量
break;
case AudioManager.AUDIOFOCUS_LOSS:
playOnAudioFocus = false;
stopPlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
playOnAudioFocus = true;
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 降低音量
break;
}
}
private void startPlayback() {
// 实现播放逻辑
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1.0f)
.build();
mediaSession.setPlaybackState(state);
}
private void pausePlayback() {
// 实现暂停逻辑
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_PAUSED, 0, 1.0f)
.build();
mediaSession.setPlaybackState(state);
}
private void stopPlayback() {
// 实现停止逻辑
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f)
.build();
mediaSession.setPlaybackState(state);
}
}
五、音频焦点在前台服务中的使用
对于需要在后台持续播放音频的应用,应该在前台服务中处理音频焦点。以下是使用 AndroidX 和通知渠道的更新版本:
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import androidx.media.app.NotificationCompat.MediaStyle;
public class AudioPlaybackService extends Service {
private static final int NOTIFICATION_ID = 1;
private static final String CHANNEL_ID = "audio_playback_channel";
private AudioFocusHelper audioFocusHelper;
private MediaPlaybackManager mediaPlaybackManager;
private NotificationManager notificationManager;
@Override
public void onCreate() {
super.onCreate();
audioFocusHelper = new AudioFocusHelper(this, this::onAudioFocusChange);
mediaPlaybackManager = new MediaPlaybackManager(this);
notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (audioFocusHelper.requestAudioFocus()) {
startForeground(NOTIFICATION_ID, createNotification());
// 开始播放
return START_STICKY;
}
return START_NOT_STICKY;
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"音频播放",
NotificationManager.IMPORTANCE_LOW);
channel.setDescription("显示音频播放控制");
notificationManager.createNotificationChannel(channel);
}
}
private Notification createNotification() {
// 创建点击通知时的Intent
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
// 创建媒体控制按钮的Intent
Intent pauseIntent = new Intent(this, AudioPlaybackService.class);
pauseIntent.setAction("ACTION_PAUSE");
PendingIntent pausePendingIntent = PendingIntent.getService(this, 0, pauseIntent, 0);
// 创建通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("正在播放音乐")
.setContentText("歌曲名称 - 艺术家")
.setSmallIcon(R.drawable.ic_music_note)
.setContentIntent(pendingIntent)
.addAction(R.drawable.ic_pause, "暂停", pausePendingIntent)
.setStyle(new MediaStyle()
.setShowActionsInCompactView(0)
.setMediaSession(mediaPlaybackManager.getMediaSessionToken()))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOngoing(true);
return builder.build();
}
@Override
public void onDestroy() {
audioFocusHelper.abandonAudioFocus();
stopForeground(true);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void onAudioFocusChange(int focusChange) {
// 处理音频焦点变化
}
}
六、ExoPlayer 中的音频焦点处理
ExoPlayer 是 Android 推荐的现代媒体播放器库,它内置了对音频焦点的支持。以下是 ExoPlayer 与音频焦点结合使用的示例:
import android.content.Context;
import android.media.AudioManager;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.upstream.DefaultLoadControl;
import androidx.media3.ui.PlayerView;
@UnstableApi
public class ExoPlayerAudioFocusManager implements Player.Listener, AudioManager.OnAudioFocusChangeListener {
private final ExoPlayer player;
private final AudioManager audioManager;
private final AudioFocusRequest audioFocusRequest;
private boolean playOnFocusGain;
private final Context context;
public ExoPlayerAudioFocusManager(Context context, PlayerView playerView) {
this.context = context;
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
// 创建ExoPlayer实例
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
DefaultLoadControl loadControl = new DefaultLoadControl();
DefaultDataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context);
player = new ExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.setLoadControl(loadControl)
.build();
playerView.setPlayer(player);
player.addListener(this);
// 设置音频属性
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
player.setAudioAttributes(audioAttributes, true);
// 创建音频焦点请求
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
android.media.AudioAttributes playbackAttributes = new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(this)
.build();
} else {
audioFocusRequest = null;
}
}
public void play(String mediaUrl) {
// 准备媒体源
MediaItem mediaItem = MediaItem.fromUri(mediaUrl);
player.setMediaItem(mediaItem);
player.prepare();
// 请求音频焦点
if (requestAudioFocus()) {
player.play();
} else {
playOnFocusGain = true;
}
}
public void pause() {
player.pause();
}
public void release() {
abandonAudioFocus();
player.release();
}
private boolean requestAudioFocus() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && audioFocusRequest != null) {
int result = audioManager.requestAudioFocus(audioFocusRequest);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
} else {
int result = audioManager.requestAudioFocus(
this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
}
private void abandonAudioFocus() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O && audioFocusRequest != null) {
audioManager.abandonAudioFocusRequest(audioFocusRequest);
} else {
audioManager.abandonAudioFocus(this);
}
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
if (playOnFocusGain) {
playOnFocusGain = false;
player.setVolume(1.0f);
player.play();
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
playOnFocusGain = false;
player.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
playOnFocusGain = true;
player.pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 降低音量而不是暂停
player.setVolume(0.3f);
break;
}
}
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
if (playWhenReady) {
requestAudioFocus();
} else {
abandonAudioFocus();
}
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
// 处理播放状态变化
}
}
七、音频焦点与蓝牙设备交互
当应用通过蓝牙设备播放音频时,需要特别处理音频焦点与蓝牙设备的交互:
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
public class BluetoothAudioManager {
private final Context context;
private final AudioManager audioManager;
private final BluetoothAdapter bluetoothAdapter;
private final AudioFocusHelper audioFocusHelper;
private boolean isBluetoothConnected = false;
private boolean wasPlayingBeforeDisconnect = false;
public BluetoothAudioManager(Context context, AudioFocusHelper audioFocusHelper) {
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
this.audioFocusHelper = audioFocusHelper;
registerBluetoothReceiver();
}
private void registerBluetoothReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
context.registerReceiver(bluetoothReceiver, filter);
}
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED);
switch (state) {
case BluetoothA2dp.STATE_CONNECTED:
onBluetoothConnected();
break;
case BluetoothA2dp.STATE_DISCONNECTED:
onBluetoothDisconnected();
break;
}
}
}
};
private void onBluetoothConnected() {
isBluetoothConnected = true;
// 检查是否应该自动恢复播放
if (wasPlayingBeforeDisconnect) {
if (audioFocusHelper.requestAudioFocus()) {
// 恢复播放
startPlayback();
wasPlayingBeforeDisconnect = false;
}
}
}
private void onBluetoothDisconnected() {
isBluetoothConnected = false;
// 如果正在播放,标记为应该在重新连接后恢复
if (isCurrentlyPlaying()) {
wasPlayingBeforeDisconnect = true;
pausePlayback();
}
}
public boolean isBluetoothAudioDevice() {
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
return false;
}
// 检查当前音频输出是否为蓝牙设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo device : devices) {
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
return true;
}
}
} else {
// 兼容旧版本
return audioManager.isBluetoothA2dpOn() || audioManager.isBluetoothScoOn();
}
return false;
}
public void handleBluetoothSco() {
// 对于需要SCO连接的应用(如语音通话)
if (bluetoothAdapter != null && bluetoothAdapter.isEnabled()) {
if (!audioManager.isBluetoothScoOn()) {
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
}
}
}
public void releaseBluetoothSco() {
if (audioManager.isBluetoothScoOn()) {
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
}
}
public void release() {
context.unregisterReceiver(bluetoothReceiver);
releaseBluetoothSco();
}
// 以下方法应由应用实现
private boolean isCurrentlyPlaying() {
// 返回当前播放状态
return false;
}
private void startPlayback() {
// 实现开始播放逻辑
}
private void pausePlayback() {
// 实现暂停播放逻辑
}
}
八、音频焦点请求失败的处理策略
在实际应用中,音频焦点请求可能会失败,我们需要有合适的处理策略:
-
立即重试策略:
public boolean requestAudioFocusWithRetry() { int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { if (audioFocusHelper.requestAudioFocus()) { return true; } retryCount++; try { Thread.sleep(100); // 等待100毫秒后重试 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } return false; } -
延迟重试策略:
private void scheduleAudioFocusRetry() { Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(() -> { if (audioFocusHelper.requestAudioFocus()) { if (playOnAudioFocus) { startPlayback(); } } }, 1000); // 1秒后重试 } -
用户触发重试:
public void handleAudioFocusFailure() { // 显示提示给用户 showPlaybackErrorDialog("无法获取音频焦点,是否重试?", () -> { if (audioFocusHelper.requestAudioFocus()) { startPlayback(); } else { showToast("获取音频焦点失败"); } }); }
九、最佳实践与注意事项
-
始终释放音频焦点:当应用不再需要播放音频时,应及时释放音频焦点,以便其他应用可以使用。
-
处理各种焦点变化:正确处理 AUDIOFOCUS_GAIN、AUDIOFOCUS_LOSS、AUDIOFOCUS_LOSS_TRANSIENT 和 AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 等各种情况。
-
使用 AndroidX 库:使用 AndroidX 中的 MediaSessionCompat 和其他兼容性类,确保在各个 Android 版本上的一致行为。
-
创建通知渠道:对于 Android 8.0 及以上版本,为前台服务创建适当的通知渠道。
-
考虑蓝牙设备:当应用通过蓝牙设备播放音频时,需要特别处理连接断开和重连的情况。
-
使用现代播放器:考虑使用 ExoPlayer 等现代媒体播放器,它们内置了对音频焦点的支持。
通过以上示例,我们可以看到音频焦点在 Android 应用中的重要性及其实现方式。合理使用音频焦点可以让应用提供更好的用户体验,与其他应用和谐共存。在实际开发中,建议根据具体场景选择合适的实现方式,并注意版本兼容性处理。
本文链接:Android 中音频焦点的使用场景及代码示例 - https://www.h89.cn/archives/476.html
版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。
评论已关闭