@DevWiki
2016-02-27T07:43:13.000000Z
字数 6872
阅读 3264
Android
最近比较忙,好久都没有更新博客!
最近由于项目需求要做音乐播放模式的切换,经过好多次尝试,发现这里面满满的都是坑啊,所以就写一篇日志记录下来,也给大家分享下遇到的坑及解决办法.
在聊天场景中,收到对方语音时,用户可以选择外放播放,也可以选择插入耳机收听.更人性化一点当用户把手机靠近耳朵时屏幕关闭自动切换到听筒中播放,播放完毕后拿开手机屏幕自动点亮.比如微信就是如此.
从上面场景中我们可以得出我们需要的要点:
从需求分析我们可以得出需要代码进行控制的有:
音乐播放控制最简单,直接使用MediaPlayer即可,为了更好地与界面代码分离以及更好控制音乐,这里写了一个控制类:PlayerManager,如下:
/*** 音乐播放管理类* Created by DevWiki on 2015/8/27 0027.*/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() {@Overridepublic 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* Created by DevWiki on 2015/9/16 0016.*/public class MyApplication extends Application {private static Context context;@Overridepublic 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{@Overridepublic 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接口,覆写其方法:
@Overridepublic 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
其中第一个参数代表控制级别,可选值有:
此处我们选取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;}}
通过以上三个解决方案,然后运行程序可知基本满足功能需求.但是有以下几个问题:
Activity的onPause(),onStop()方法!!此问题只需在耳机模式下对距离感应器不做响应即可,在PlayerManager中添加:
/*** 耳机是否插入* @return 插入耳机返回true,否则返回false*/@SuppressWarnings("deprecation")public boolean isWiredHeadsetOn(){return audioManager.isWiredHeadsetOn();}
然后修改距离感应器回调方法为:
@Overridepublic 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()中就要重新获取,防止出现其他问题.
项目代码请点击此处:PlayMode
我的更多信息:DevWiki