Android音樂播放模式切換-外放、聽筒、耳機
https://blog.csdn.net/u010936731/article/details/70599482/
場景需求
在聊天場景中,收到對方語音時,用戶可以選擇外放播放,也可以選擇插入耳機收聽.更人性化一點當用戶把手機靠近耳朵時屏幕關閉自動切換到聽筒中播放,播放完畢後拿開手機屏幕自動點亮.比如微信就是如此.
需求分析
從上面場景中我們可以得出我們需要的要點:
播放模式切換:外放<—>耳機
播放模式切換:外放<—>聽筒
屏幕操作:亮屏<—>息屏<—>亮屏
解決問題
從需求分析我們可以得出需要代碼進行控制的有:
音樂播放控制
外放,耳機,聽筒之間的切換
屏幕的息屏與亮屏
音樂播放控制
音樂播放控制最簡單,直接使用MediaPlayer即可,爲了更好地與界面代碼分離以及更好控制音樂,這裏寫了一個控制類:PlayerManager,如下:
/**
* 音樂播放管理類
*/
public class PlayerManager {
private static PlayerManager playerManager;
private MediaPlayer mediaPlayer;
private PlayCallback callback;
private Context context;
private String filePath;
public static PlayerManager getManager(){
if (playerManager == null){
synchronized (PlayerManager.class){
playerManager = new PlayerManager();
}
}
return playerManager;
}
private PlayerManager(){
this.context = MyApplication.getContext();
mediaPlayer = new MediaPlayer();
}
/**
* 播放回調接口
*/
public interface PlayCallback{
/** 音樂準備完畢 */
void onPrepared();
/** 音樂播放完成 */
void onComplete();
/** 音樂停止播放 */
void onStop();
}
/**
* 播放音樂
* @param path 音樂文件路徑
* @param callback 播放回調函數
*/
public void play(String path, final PlayCallback callback){
this.filePath = path;
this.callback = callback;
try {
mediaPlayer.reset();
mediaPlayer.setDataSource(context, Uri.parse(path));
mediaPlayer.prepare();
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
callback.onPrepared();
mediaPlayer.start();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 停止播放
*/
public void stop(){
if (isPlaying()){
try {
mediaPlayer.stop();
callback.onStop();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
/**
* 是否正在播放
* @return 正在播放返回true,否則返回false
*/
public boolean isPlaying() {
return mediaPlayer != null && mediaPlayer.isPlaying();
}
}
爲了方便獲取Context,覆寫了Application類如下:
/**
* APP的Application
*/
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = this;
}
/**
* 獲取APP的Context方便其他地方調用
* @return
*/
public static Context getContext(){
return context;
}
}
外放,耳機,聽筒之間的切換
在Android系統中是用AudioManager來管理播放模式的,通過AudioManager.setMode()方法來實現.
在setMode()方法中有以下幾種對應不同的播放模式:
MODE_NORMAL: 普通模式,既不是鈴聲模式也不是通話模式
MODE_RINGTONE:鈴聲模式
MODE_IN_CALL:通話模式
MODE_IN_COMMUNICATION:通信模式,包括音/視頻,VoIP通話.(3.0加入的,與通話模式類似)
其中:
播放音樂的對應的就是MODE_NORMAL, 如果使用外放播則調用audioManager.setSpeakerphoneOn(true)即可.
若使用耳機和聽筒,則需要先設置模式爲MODE_IN_CALL(3.0以前)或MODE_IN_COMMUNICATION(3.0以後).
注意:
需要權限android.permission.MODIFY_AUDIO_SETTINGS
爲什麼在3.0以後設置模式爲MODE_IN_COMMUNICATION,而不設置爲MODE_IN_CALL?
經驗證在華爲的某些機型中,設置MODE_IN_CALL根本不起作用.
故在PlayerManager類中持有一個AudioManager變量,並添加如下幾個方法:
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
/**
* 切換到外放
*/
public void changeToSpeaker(){
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(true);
}
/**
* 切換到耳機模式
*/
public void changeToHeadset(){
audioManager.setSpeakerphoneOn(false);
}
/**
* 切換到聽筒
*/
public void changeToReceiver(){
audioManager.setSpeakerphoneOn(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
} else {
audioManager.setMode(AudioManager.MODE_IN_CALL);
}
}
如何判斷用戶是否插入耳機呢?
在插入或者拔出耳機時系統會發出Action爲Intent.ACTION_HEADSET_PLUG的廣播,並且該廣播不能使用靜態接收器處理,故寫一個廣播接收器處理耳機事件即可.
class HeadsetReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action){
//插入和拔出耳機會觸發此廣播
case Intent.ACTION_HEADSET_PLUG:
int state = intent.getIntExtra("state", 0);
if (state == 1){
playerManager.changeToHeadset();
} else if (state == 0){
playerManager.changeToSpeaker();
}
break;
default:
break;
}
}
}
屏幕的息屏與亮屏
屏幕息屏與亮屏有個前提是正確判斷用戶是否靠近聽筒,如何判斷?
現在幾乎每個手機都有距離感應器,通過舉例感應器可獲得距離.距離感應器由SensorManager管理:
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL);
註冊監聽的方法的最後一個參數是敏感度,敏感度越高越費電,此處選擇一般敏感度即可.此外Activity還需實現SensorEventListener接口,覆寫其方法:
@Override
public void onSensorChanged(SensorEvent event) {
float value = event.values[0];
if (playerManager.isPlaying()){
if (value == sensor.getMaximumRange()) {
playerManager.changeToSpeaker();
setScreenOn();
} else {
playerManager.changeToReceiver();
setScreenOff();
}
} else {
if(value == sensor.getMaximumRange()){
playerManager.changeToSpeaker();
setScreenOn();
}
}
}
在Android系統中硬件的工作狀態的控制由PowerManager與WakeLock掌管.PowerManager通過不同的WakeLock來控制CPU,屏幕,鍵盤等硬件的工作狀態.
powerManager = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
注意:需要權限android.Manifest.permission.DEVICE_POWER和android.permission.WAKE_LOCK
其中第一個參數代表控制級別,可選值有:
PARTIAL_WAKE_LOCK : CPU運行,屏幕和鍵盤可能關閉
SCREEN_DIM_WAKE_LOCK : 屏幕亮,鍵盤燈可能關閉
SCREEN_BRIGHT_WAKE_LOCK : 屏幕全亮,鍵盤燈可能關閉
FULL_WAKE_LOCK : 屏幕和鍵盤燈全亮
PROXIMITY_SCREEN_OFF_WAKE_LOCK : 屏幕關閉,鍵盤燈關閉,CPU運行
DOZE_WAKE_LOCK : 屏幕灰顯,CPU延緩工作
此處我們選取5.PROXIMITY_SCREEN_OFF_WAKE_LOCK.WakeLock通過acquire()和release()方法上鎖和解鎖.
private void setScreenOff(){
if (wakeLock == null){
wakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
}
wakeLock.acquire();
}
private void setScreenOn(){
if (wakeLock != null){
wakeLock.setReferenceCounted(false);
wakeLock.release();
wakeLock = null;
}
}
開始驗證
通過以上三個解決方案,然後運行程序可知基本滿足功能需求.但是有以下幾個問題:
耳機模式下用手遮擋距離感應器會切換到聽筒
三星Note,華爲P,華爲Mate系列會出現外放切換到聽筒,聽筒切換到外放出現卡頓現象
耳機切換到外放會出現丟失語音
三星,華爲手機在熄滅屏幕是會調用Activity的onPause(),onStop()方法!
解決新問題
耳機模式用手遮擋距離感應器問題
此問題只需在耳機模式下對距離感應器不做響應即可,在PlayerManager中添加:
/**
* 耳機是否插入
* @return 插入耳機返回true,否則返回false
*/
@SuppressWarnings("deprecation")
public boolean isWiredHeadsetOn(){
return audioManager.isWiredHeadsetOn();
}
然後修改距離感應器回調方法爲:
@Override
public void onSensorChanged(SensorEvent event) {
float value = event.values[0];
if (playerManager.isWiredHeadsetOn()){
return;
}
if (playerManager.isPlaying()){
if (value == sensor.getMaximumRange()) {
playerManager.changeToSpeaker();
setScreenOn();
} else {
playerManager.changeToReceiver();
setScreenOff();
}
} else {
if(value == sensor.getMaximumRange()){
playerManager.changeToSpeaker();
setScreenOn();
}
}
}
三星,華爲聽筒外放切換卡頓
這個問題只能採用折中的辦法:重新播放
爲何採用此方法?
短的語音本來就短,切換重播幾乎不受影響
長得音樂一般不會用聽筒聽
不是所有的手機都會出現卡頓
故在PlayerManager中修改方法:
/**
* 切換到聽筒
*/
public void changeToReceiver(){
if (isPlaying()){
stop();
changeToReceiverNoStop();
play(filePath, callback);
} else {
changeToReceiverNoStop();
}
}
/**
* 切換到外放
*/
public void changeToSpeaker(){
if (PhoneModelUtil.isSamsungPhone() || PhoneModelUtil.isHuaweiPhone()){
stop();
changeToSpeakerNoStop();
play(filePath, callback);
} else {
changeToSpeakerNoStop();
}
}
public void changeToSpeakerNoStop(){
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(true);
}
耳機切換到外放會出現丟失語音
此問題由於耳機切換到外放需要一段時間導致,故解決此問題的方法是先暫停再續播.那麼什麼時候暫停什麼時候續播呢?
查資料得知,在耳機拔出時系統還會發出Action爲AudioManager.ACTION_AUDIO_BECOMING_NOISY的廣播,且此廣播比Intent.ACTION_HEADSET_PLUG要早,所以解決方案也出來了:
收到AudioManager.ACTION_AUDIO_BECOMING_NOISY時暫停播放
收到Intent.ACTION_HEADSET_PLUG並且附帶的state=1時續播
三星,華爲手機在熄滅屏幕是會調用Activity的onPause(),onStop()方法
這個問題嘛,其實也不算問題,但是值得注意.如果你在onStop()中做了某些釋放資源的操作,那麼在onStart()中就要重新獲取,防止出現其他問題.