一:首先我们来了解下什么是AudioFocus:

AudioFocus是Android引入的一个Audio协调机制,当多方需要使用Audio资源时,可以通过AudioFocus机制来协调配合,提高用户的体验。

需要注意的一点是:该机制需要开发者主动去遵守,比如A应用没遵守该机制,则其它遵守了该机制的应用是完全没办法影响A应用的。


二:为什么要使用AudioFocus:

试想下后台在播放着音乐的时候你点开了某个视频,使得后台的音乐和视频的声音一起播放,毫无关联的声音一同播放会给用户带来极差的体验,此时我们就可以通过AudioFocus机制来解决这样的问题。现在市面上的网易云音乐、虾米音乐等主流音乐应用都有遵循该机制。


三:AudioFocus的使用:


我们先来看下没有用AudioFocus的场景:

在Activity中定义了4个Button,第一第二个按钮分别用于开始和暂停手机中的第一首音乐,第三和第四个按钮分别用于开始和暂停手机中的第二首音乐。

......

    protected Player mPlayerOne;

    protected Player mPlayerTwo;

    ......

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_start_one:
                mPlayerOne.startMedia();
                break;
            case R.id.button_pause_one:
                mPlayerOne.pauseMedia();
                break;
            case R.id.button_start_two:
                mPlayerTwo.startMedia();
                break;
            case R.id.button_pause_two:
                mPlayerTwo.pauseMedia();
                break;
            default:
                break;
        }
    }
public class Player {

    ......

    private MediaPlayer mMediaPlayer;

    public void startMedia() {
        mMediaPlayer.start();
    }

    public void pauseMedia() {
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    }

}

如果我们点击button_start_one播放第一首音乐之后,再点击button_start_two播放第二首音乐,会使得两首音乐同时播放,这样的用户体验非常不好的。

接下来我们就在这基础上加入AudioFocus机制,从而使音乐一和音乐二的播放能相互配合。


在加入之前,我们先来了解下AudioFocus机制的相关知识:

使用AudioFocus机制主要是通过android.media.AudioManager这个类来进行的。

1. 申请音频焦点

在这个类中通过

requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint)

方法请求获取焦点,如果获取成功,会返回int值AudioManager.AUDIOFOCUS_REQUEST_GRANTED,失败则返回AudioManager.AUDIOFOCUS_REQUEST_FAILED

通过abandonAudioFocus(OnAudioFocusChangeListener l)方法放弃焦点。

由于音频焦点是唯一的,所以我们可以在需要播放音乐时去申请音频焦点,如果获取到了则播放,同时正在播放的音频在失去音频焦点时停止播放或者调低音量,从而达到音频播放间的相互协调。

接下来我们对requestAudioFocus方法的参数进行解析:

OnAudioFocusChangeListener

OnAudioFocusChangeListener是一个接口,在这个接口里面只有一个方法需要实现
public void onAudioFocusChange(int focusChange);该方法会在焦点状态变化的时候被调用。

参数focusChange代表变化后当前的状态,一共有以下四个值:

  • AUDIOFOCUS_GAIN
    重新获取到音频焦点时触发的状态。
  • AUDIOFOCUS_LOSS
    失去音频焦点时触发的状态,且该状态应该会长期保持,此时应当暂停音频并释放音频相关的资源。
  • AUDIOFOCUS_LOSS_TRANSIENT
    失去音频焦点时触发的状态,但是该状态不会长时间保持,此时我们应该暂停音频,且当重新获取音频焦点的时候继续播放。
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
    失去音频焦点时触发的状态,在该状态的时候不需要暂停音频,但是我们应该降低音频的声音。

OnAudioFocusChangeListener还有一个非常重要的作用,就是它的实例作为每一个申请音频焦点的“单位”。通过查看源码可以发现每次申请音频焦点时,都会把这次申请存进一个HashMap<String, OnAudioFocusChangeListener> 当中,并通过调用String getIdForAudioFocusListener(OnAudioFocusChangeListener l) 方法获取每个OnAudioFocusChangeListener 对应的key,接下来获取音频焦点等操作都是基于这个key的。而放弃音频焦点时则会将对应的OnAudioFocusChangeListenerHashMap<String, OnAudioFocusChangeListener>中移除,同时基于该接口的key来实现放弃音频焦点的操作。

streamType
streamType大家应该都很熟悉,就不多介绍了,需要注意的一点是不同的streamType之间的音频焦点是不会相互影响的。

durationHint
durationHint用来表示获取焦点的时长,同时通知其它获取了音频焦点的OnAudioFocusChangeListener该如何相互配合,有以下几个值:

  • AUDIOFOCUS_GAIN
    代表此次申请的音频焦点需要长时间持有,原本获取了音频焦点的OnAudioFocusChangeListener 接口将会回调onAudioFocusChange(int focusChange) 方法,传入的参数为AUDIOFOCUS_LOSS。
  • AUDIOFOCUS_GAIN_TRANSIENT
    代表此次申请的音频焦点只需短暂持有,原本获取了音频焦点的OnAudioFocusChangeListener 接口将会回调onAudioFocusChange(int focusChange) 方法,传入的参数为AUDIOFOCUS_LOSS_TRANSIENT。按照官方注释:在接收到事件通知等情景时可使用该durationHint。
  • AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
    代表此次申请的音频焦点只需短暂持有,原本获取了音频焦点的OnAudioFocusChangeListener 接口将会回调onAudioFocusChange(int focusChange) 方法,传入的参数为AUDIOFOCUS_LOSS_TRANSIENT。按照官方注释:在需要录音或语音识别等情景时可使用该durationHint。
  • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
    代表此次申请不需要暂停其它申请的音频播放,但是需要其降低音量。原本获取了音频焦点的OnAudioFocusChangeListener 接口将会回调onAudioFocusChange(int focusChange) 方法,传入的参数为AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK。

2. 释放音频焦点

通过public int abandonAudioFocus(OnAudioFocusChangeListener l) 方法释放音频焦点,参数只有一个OnAudioFocusChangeListener

在讲解requestAudioFocus 方法的时候就已经说过音频焦点的机制中是以OnAudioFocusChangeListener 为单位的,因此在abandonAudioFocus 方法中的参数需要与requestAudioFocus 方法对应起来,才能成功释放。


3. 进行修改

在简单了解了AudioFocus的基本知识之后,我们就可以来动手修改了。

先是修改Activity的代码:

......

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_start_one:
                mPlayerOne.startMediaWithAudioFocus();
                break;
            case R.id.button_pause_one:
                mPlayerOne.pauseMediaWithAudioFocus();
                break;
            case R.id.button_start_two:
                mPlayerTwo.startMediaWithAudioFocus();
                break;
            case R.id.button_pause_two:
                mPlayerTwo.pauseMediaWithAudioFocus();
                break;
            default:
                break;
        }
    }

    ......

然后修改Play.java

public class Player {

    ......

    private MediaPlayer mMediaPlayer;

    private AudioManager mAudioManager; 

    private MyAudioFocusChangeListener mFocusChangeListener; 

    ......

    public void startMediaWithAudioFocus() {
        if (mMediaPlayer == null) {
            initPlayer();
        }
        int result = mAudioManager.requestAudioFocus(mFocusChangeListener, AudioManager.STREAM_MUSIC,
                AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            mMediaPlayer.start();
        }
    }

    public void pauseMediaWithAudioFocus() {
        mAudioManager.abandonAudioFocus(mFocusChangeListener);
        if (mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    } 

    ...... 


class MyAudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener {

        private int mPreviousState;

        private boolean mShouldStart = true;

        @Override
        public void onAudioFocusChange(int focusChange) {
            handlerAudioFocus(focusChange);
            mPreviousState = focusChange;
        }

        private void handlerAudioFocus(int focusChange) {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    handlerAudioFocusGain();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS:
                    mMediaPlayer.release();
                    mMediaPlayer = null;
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    mMediaPlayer.pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    mMediaPlayer.setVolume(0.5f,0.5f);
                    break;
            }
        }

        private void handlerAudioFocusGain() {
            switch (mPreviousState) {
                case AudioManager.AUDIOFOCUS_LOSS:
                    initPlayer();
                    mShouldStart = false;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    if (mShouldStart) {
                        mMediaPlayer.start();
                    } else {
                        mShouldStart = true;
                    }
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    mMediaPlayer.setVolume(1,1);
                    break;
                default:
            }
        }
    }

}

在开始播放音乐之前,我们先是申请了音频焦点,durationHint为AUDIOFOCUS_GAIN_TRANSIENT,并且在成功获取到音频焦点后播放音乐。

在暂停音乐的时候我们会去释放音频焦点,通知原先的音频焦点申请者可以继续播放音乐。

接下来我们看下MyAudioFocusChangeListener 里面的代码,在onAudioFocusChange回调的时候我们通过handlerAudioFocus 方法来进行处理。

AudioManager.AUDIOFOCUS_GAIN

当focusChange为AudioManager.AUDIOFOCUS_GAIN时,我们会调用handlerAudioFocusGain() 方法来处理,在这个方法中会根据上一次的音频焦点状态来做恢复操作,比如上次的状态为AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK ,则把音量调回去正常的大小,上次的状态为AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ,则重新播放音频,上次的状态为AudioManager.AUDIOFOCUS_LOSS 则重新初始化音乐资源。

细心的朋友应该会发现handlerAudioFocusGain()方法的每个case之间并没有用break跳出,这样做的原因是Loss状态中,资源释放度高的状态能够在资源释放度低的状态被触发后接着触发。举个栗子:当前有一个A应用正在播放音乐,这时候一个B应用请求了音频焦点使得当前接收到了AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 从而导致A应用音量调低,过了一会B应用通过同一个AudioManager.OnAudioFocusChangeListener 又请求了音频焦点,使得当前音乐接收到了AudioManager.AUDIOFOCUS_LOSS_TRANSIENT 并暂停了音乐播放。过了一段时间后A应用释放了音频焦点,此时B应用会回调对应的AudioManager.OnAudioFocusChangeListeneronAudioFocusChange方法,此时记录的上一个状态为AudioManager.AUDIOFOCUS_LOSS_TRANSIENT。所以如果我们在每个case中都添加了break语句,则在上面所述的栗子中B应用重新播放音乐的时候被调低声音的音乐将无法被恢复。

AudioManager.AUDIOFOCUS_LOSS
当focusChange为AudioManager.AUDIOFOCUS_LOSS 时,我们将音乐资源进行了释放。

AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
当focusChange为AudioManager.AUDIOFOCUS_LOSS_TRANSIENT 时,我们使音乐暂停。

AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
当focusChange为AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 时,我们调低了音乐的音量。

本篇博客到此结束,如果有错漏欢迎提出,谢谢。