乐趣区

浅谈Android O Touch声音播放流程

前言
当我们点击屏幕按键时,就会听到 touch 音,那么 touch 音是如何播放起来的呢,由于最近项目需求顺便熟悉下了 touch 音的逻辑。
正文
谈 touch 逻辑首先要说下这个类 ViewRootImpl.java,位于 frameworks/base/core/java/android/view 下,ViewRootImpl 的主要功能:A:链接 WindowManager 和 DecorView 的纽带,更广一点可以说是 Window 和 View 之间的纽带。B:完成 View 的绘制过程,包括 measure、layout、draw 过程。C:向 DecorView 分发收到的用户发起的 event 事件,如按键,触屏等事件。关于 ViewRootImpl 的源码可参照博客 ViewRootImpl 类源码解析,我们从 performFocusNavigation() 入手
private boolean performFocusNavigation(KeyEvent event) {
// 略
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
return true;
}
// 略
return false;
}
当我们点击某个控件时,会先触发 performFocusNavigation() 这个方法,然后当控件获取到 focus 后便会调用 playSoundEffect() 方法,我只截取了 performFocusNavigation() 中关键代码 playSoundEffect() 部分,来看下 playSoundEffect() 这个方法
public void playSoundEffect(int effectId) {
checkThread();

try {
final AudioManager audioManager = getAudioManager();

switch (effectId) {
case SoundEffectConstants.CLICK:
audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
return;
case SoundEffectConstants.NAVIGATION_DOWN:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
return;
case SoundEffectConstants.NAVIGATION_LEFT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
return;
case SoundEffectConstants.NAVIGATION_RIGHT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
return;
case SoundEffectConstants.NAVIGATION_UP:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
return;
default:
throw new IllegalArgumentException(“unknown effect id ” + effectId +
” not defined in ” + SoundEffectConstants.class.getCanonicalName());
}
} catch (IllegalStateException e) {
// Exception thrown by getAudioManager() when mView is null
Log.e(mTag, “FATAL EXCEPTION when attempting to play sound effect: ” + e);
e.printStackTrace();
}
}
发现调用了 audioManager 的 playSoundEffect() 方法,audiomanager 就不说了,接触 android audio 最先接触的可能就是 AudioManager 了,音量控制,声音焦点申请等。接着看
public void playSoundEffect(int effectType) {
if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
return;
}
// 查询是否开启 touch 音,如果 settings 中关闭了,则直接返回
if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
return;
}

final IAudioService service = getService();
try {
// 调用到 AudioService 的 playSoundEffect()
service.playSoundEffect(effectType);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
查询 touch 音是否可播放,因为毕竟在 android 的 setting 中有个 touch 音的开关,如果可播放则调用到 AudioService 的 playSoundEffect()
public void playSoundEffect(int effectType) {
playSoundEffectVolume(effectType, -1.0f);
}

public void playSoundEffectVolume(int effectType, float volume) {
if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
Log.w(TAG, “AudioService effectType value ” + effectType + ” out of range”);
return;
}

sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,
effectType, (int) (volume * 1000), null, 0);
}
其实 AudioService 初始化的时候会创建一个子线 HandlerThread,HandlerThread 主要处理一些相对耗时的操作,这里将播放 touch 音的功能放在了这个子线程中去执行,这样避免了主线程的阻塞,其实大家在做 mediaplayer 播放时也建议放在子线程去播放,接下来看看 handler 里对消息的处理,关键代码如下
case MSG_PLAY_SOUND_EFFECT:
if (msg.obj == null) {
onPlaySoundEffect(msg.arg1, msg.arg2, 0);
} else {
onPlaySoundEffect(msg.arg1, msg.arg2, (int) msg.obj);
}
break;
直接调用 onPlaySoundEffect() 的方法
private void onPlaySoundEffect(int effectType, int volume) {
synchronized (mSoundEffectsLock) {
// 初始化 mSoundPool 和要播放的资源文件
onLoadSoundEffects();

if (mSoundPool == null) {
return;
}
float volFloat;
// use default if volume is not specified by caller
if (volume < 0) {
volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
} else {
volFloat = volume / 1000.0f;
}

if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
// 播放 touch 音
mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
volFloat, volFloat, 0, 0, 1.0f);
} else {
MediaPlayer mediaPlayer = new MediaPlayer();
try {
String filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH +
SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);
mediaPlayer.setDataSource(filePath);
mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
mediaPlayer.prepare();
mediaPlayer.setVolume(volFloat);
mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
public void onCompletion(MediaPlayer mp) {
cleanupPlayer(mp);
}
});
mediaPlayer.setOnErrorListener(new OnErrorListener() {
public boolean onError(MediaPlayer mp, int what, int extra) {
cleanupPlayer(mp);
return true;
}
});
mediaPlayer.start();
} catch (IOException ex) {
Log.w(TAG, “MediaPlayer IOException: “+ex);
} catch (IllegalArgumentException ex) {
Log.w(TAG, “MediaPlayer IllegalArgumentException: “+ex);
} catch (IllegalStateException ex) {
Log.w(TAG, “MediaPlayer IllegalStateException: “+ex);
}
}
}
}
最终通过 soundPool 来播放指定的资源文件实现了 touch 音的播放,因此大家在工作中如果有什么需要对应 touch 音的逻辑,可参照 AudioService 的 onPlaySoundEffect() 中的逻辑。比如指定 touch 音的 AudioAttributes 使 touch 音输出到指定的 device 上等。
总结
touch 音的流程就简单分析到这里,欢迎大家交流指正。努力学习 ing~

退出移动版