Android9 framework 按键音调用流程及自定义按键音(替换原生按键音)和调节按键音音量的方法
一、按键音调用流程
摘要:按键音播放的总体逻辑是先找到系统中按键音的资源,然后调用SoundPool.load让系统加载音频资源,加载成功后在onLoadComplete回调中会返回一个非0的soundID ,用于播放时指定特定的音频,最后在需要播放按键音的时候直接根据soundID播放
1.Android按键音接口
Android按键音只有两个常用接口,分别是:
- 原生设置APP中SoundFragment.java调用的设置按键音开关的接口:mAudioManager.loadSoundEffects()和mAudioManager.unloadSoundEffects()
private void setSoundEffectsEnabled(boolean enabled) { mAudioManager = (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); //1 if (enabled) { mAudioManager.loadSoundEffects(); } else { mAudioManager.unloadSoundEffects(); } Settings.System.putInt(getActivity().getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, enabled ? 1 : 0); }
先调用AudioManager的loadSoundEffects方法,然后会调用到AudioService的loadSoundEffects方法
- View.java中播放按键音的接口:playSoundEffect
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK);//调用会经过ViewRootImpl.java,最终调用到AudioService中 li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
最终会调用到AudioService的playSoundEffect方法
2.onLoadSoundEffects()方法
上述的两个方法调用到AudioService之后,分别通过sendMsg向handler发送MSG_LOAD_SOUND_EFFECTS和MSG_PLAY_SOUND_EFFECT信息,handler在收到信息后会进行相应的操作,但是不管是哪个操作,都会调用到onLoadSoundEffects()方法
loadSoundEffects的调用流程(非重点):
public boolean loadSoundEffects() { int attempts = 3; LoadSoundEffectReply reply = new LoadSoundEffectReply(); synchronized (reply) { //调用sendMsg方法 sendMsg(mAudioHandler, MSG_LOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, reply, 0); while ((reply.mStatus == 1) && (attempts-- > 0)) { try { reply.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); } catch (InterruptedException e) { Log.w(TAG, "loadSoundEffects Interrupted while waiting sound pool loaded."); } } } return (reply.mStatus == 0); }//sendMsg方法是对handler.sendMessageAtTime的封装 private static void sendMsg(Handler handler, int msg, int existingMsgPolicy, int arg1, int arg2, Object obj, int delay) { if (existingMsgPolicy == SENDMSG_REPLACE) { handler.removeMessages(msg); } else if (existingMsgPolicy == SENDMSG_NOOP && handler.hasMessages(msg)) { return; } synchronized (mLastDeviceConnectMsgTime) { long time = SystemClock.uptimeMillis() + delay; if (msg == MSG_SET_A2DP_SRC_CONNECTION_STATE || msg == MSG_SET_A2DP_SINK_CONNECTION_STATE || msg == MSG_SET_HEARING_AID_CONNECTION_STATE || msg == MSG_SET_WIRED_DEVICE_CONNECTION_STATE || msg == MSG_A2DP_DEVICE_CONFIG_CHANGE || msg == MSG_BTA2DP_DOCK_TIMEOUT) { if (mLastDeviceConnectMsgTime >= time) { // add a little delay to make sure messages are ordered as expected time = mLastDeviceConnectMsgTime + 30; } mLastDeviceConnectMsgTime = time; } handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time); } }//在handleMessage中处理消息@Override public void handleMessage(Message msg) { ...... case MSG_PLAY_SOUND_EFFECT: //调用onPlaySoundEffect onPlaySoundEffect(msg.arg1, msg.arg2); break;} private void onPlaySoundEffect(int effectType, int volume) { synchronized (mSoundEffectsLock) { //最终会调用到onLoadSoundEffects onLoadSoundEffects(); ...... }
playSoundEffect的调用流程(非重点):
public void playSoundEffect(int effectType) { playSoundEffectVolume(effectType, -1.0f); } public void playSoundEffectVolume(int effectType, float volume) { // do not try to play the sound effect if the system stream is muted if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) { return; } 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); }//在handleMessage中处理消息@Override public void handleMessage(Message msg) { ...... case MSG_PLAY_SOUND_EFFECT: onPlaySoundEffect(msg.arg1, msg.arg2); break;} private void onPlaySoundEffect(int effectType, int volume) { synchronized (mSoundEffectsLock) { onLoadSoundEffects(); ...... }
如上所述最终都会调用onLoadSoundEffects方法
在onLoadSoundEffects方法中主要完成以下几件事:
- 调用loadTouchSoundAssets方法解析XML文件,获得音源文件名,初始化数组,将音源文件与数组中元素一一对应
- 补全音源文件路径,调用SoundPool.load方法
- 将SoundPool.load方法返回的sampleId保存在数组中,作为之后play方法的参数
先来看loadTouchSoundAssets方法,代码如下:
private void loadTouchSoundAssets() { XmlResourceParser parser = null; // only load assets once. //SOUND_EFFECT_FILES是一个存放字符串的List,里面存放的是音频资源的名称 if (!SOUND_EFFECT_FILES.isEmpty()) { return; }//此方法执行://1.SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); 向SOUND_EFFECT_FILES添加一个音频资源的名称//2.初始化一个二维数组SOUND_EFFECT_FILES_MAP。行数为10,列数为2,第一列都为0,第二列都为-1 loadTouchSoundAssetDefaults(); try { //获得XML对象 parser = mContext.getResources().getXml(com.android.internal.R.xml.audio_assets); XmlUtils.beginDocument(parser, TAG_AUDIO_ASSETS); //getAttributeValue方法用于获取传入的Attribute名称对应的Value,这里是"1.0" String version = parser.getAttributeValue(null, ATTR_VERSION); boolean inTouchSoundsGroup = false; if (ASSET_FILE_VERSION.equals(version)) { while (true) { //nextElement方法用于切换到XML的下一层 XmlUtils.nextElement(parser); //获取当前parser的名称,这里是"group" String element = parser.getName(); if (element == null) { break; } if (element.equals(TAG_GROUP)) { String name = parser.getAttributeValue(null, ATTR_GROUP_NAME); if (GROUP_TOUCH_SOUNDS.equals(name)) { inTouchSoundsGroup = true; break; } } } //遍历XML中剩下的所有元素 while (inTouchSoundsGroup) { XmlUtils.nextElement(parser); String element = parser.getName(); if (element == null) { break; } if (element.equals(TAG_ASSET)) { String id = parser.getAttributeValue(null, ATTR_ASSET_ID); String file = parser.getAttributeValue(null, ATTR_ASSET_FILE); int fx; try { //getField的对象是.class文件(.java文件的预编译产物,只进行一些变量即宏的替换),这里即AudioManager.class //根据传入的id获得AudioManager.class中对应的对象,例如传入的是"FX_KEY_CLICK",得到的是AudioManager中定义的public static final int FX_KEY_CLICK = 0 Field field = AudioManager.class.getField(id); fx = field.getInt(null); } catch (Exception e) { Log.w(TAG, "Invalid touch sound ID: "+id); continue; }//根据之前XML中读取的file取出其在SOUND_EFFECT_FILES的位置//此时SOUND_EFFECT_FILES只有一个元素即"Effect_Tick.ogg"//如果不存在则加入到SOUND_EFFECT_FILES中 int i = SOUND_EFFECT_FILES.indexOf(file); if (i == -1) { i = SOUND_EFFECT_FILES.size(); SOUND_EFFECT_FILES.add(file); } SOUND_EFFECT_FILES_MAP[fx][0] = i; } else { break; } } } } catch (Resources.NotFoundException e) { Log.w(TAG, "audio assets file not found", e); } catch (XmlPullParserException e) { Log.w(TAG, "XML parser exception reading touch sound assets", e); } catch (IOException e) { Log.w(TAG, "I/O exception reading touch sound assets", e); } finally { if (parser != null) { parser.close(); } } } private void loadTouchSoundAssetDefaults() { SOUND_EFFECT_FILES.add("Effect_Tick.ogg"); for (int i = 0; i < AudioManager.NUM_SOUND_EFFECTS; i++) { SOUND_EFFECT_FILES_MAP[i][0] = 0; SOUND_EFFECT_FILES_MAP[i][1] = -1; } }
经过loadTouchSoundAssets初始化后,SOUND_EFFECT_FILES数组为:
{ "Effect_Tick.ogg" , "KeypressStandard.ogg" , "KeypressSpacebar.ogg" , "KeypressDelete.ogg" , "KeypressReturn.ogg" , "KeypressInvalid.ogg" }
SOUND_EFFECT_FILES_MAP数组为:
{ { 0, -1}, { 0, -1}, { 0, -1}, { 0, -1}, { 0, -1}, { 1, -1}, { 2, -1}, { 3, -1}, { 4, -1}, { 5, -1}}
再来看真正的onLoadSoundEffects方法:
private boolean onLoadSoundEffects() { int status; synchronized (mSoundEffectsLock) { if (!mSystemReady) { Log.w(TAG, "onLoadSoundEffects() called before boot complete"); return false; } if (mSoundPool != null) { return true; } loadTouchSoundAssets();//根据XML文件初始化数组,如上所述//初始化SoundPool mSoundPool = new SoundPool.Builder() .setMaxStreams(NUM_SOUNDPOOL_CHANNELS) .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build()) .build(); mSoundPoolCallBack = null; mSoundPoolListenerThread = new SoundPoolListenerThread(); //这个线程以及下面的代码主要是去设置SoundPoolCallback,不求甚解 mSoundPoolListenerThread.start(); int attempts = 3; while ((mSoundPoolCallBack == null) && (attempts-- > 0)) { try { // Wait for mSoundPoolCallBack to be set by the other thread mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); } catch (InterruptedException e) { Log.w(TAG, "Interrupted while waiting sound pool listener thread."); } } if (mSoundPoolCallBack == null) { Log.w(TAG, "onLoadSoundEffects() SoundPool listener or thread creation error"); if (mSoundPoolLooper != null) { mSoundPoolLooper.quit(); mSoundPoolLooper = null; } mSoundPoolListenerThread = null; mSoundPool.release(); mSoundPool = null; return false; } /* * poolId table: The value -1 in this table indicates that corresponding * file (same index in SOUND_EFFECT_FILES[] has not been loaded. * Once loaded, the value in poolId is the sample ID and the same * sample can be reused for another effect using the same file. */ //创建一个和SOUND_EFFECT_FILES一样大的数组并将元素初始化为-1 int[] poolId = new int[SOUND_EFFECT_FILES.size()]; for (int fileIdx = 0; fileIdx < SOUND_EFFECT_FILES.size(); fileIdx++) { poolId[fileIdx] = -1; } /* * Effects whose value in SOUND_EFFECT_FILES_MAP[effect][1] is -1 must be loaded. * If load succeeds, value in SOUND_EFFECT_FILES_MAP[effect][1] is > 0: * this indicates we have a valid sample loaded for this effect. */ int numSamples = 0; for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { // Do not load sample if this effect uses the MediaPlayer if (SOUND_EFFECT_FILES_MAP[effect][1] == 0) { continue; } //第一次走到这里时这个判断一定为真,因为poolId中所有元素都为-1 if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) { //getSoundEffectFilePath会根据SOUND_EFFECT_FILES中的内容补全出音频文件的具体路径 String filePath = getSoundEffectFilePath(effect); //调用SoundPool.load方法,返回的sampleId被保存在SOUND_EFFECT_FILES_MAP和poolId中 int sampleId = mSoundPool.load(filePath, 0); if (sampleId <= 0) { Log.w(TAG, "Soundpool could not load file: "+filePath); } else { SOUND_EFFECT_FILES_MAP[effect][1] = sampleId; poolId[SOUND_EFFECT_FILES_MAP[effect][0]] = sampleId; numSamples++; } } else { SOUND_EFFECT_FILES_MAP[effect][1] = poolId[SOUND_EFFECT_FILES_MAP[effect][0]]; } } // wait for all samples to be loaded if (numSamples > 0) { mSoundPoolCallBack.setSamples(poolId); attempts = 3; status = 1; while ((status == 1) && (attempts-- > 0)) { try { mSoundEffectsLock.wait(SOUND_EFFECTS_LOAD_TIMEOUT_MS); status = mSoundPoolCallBack.status(); } catch (InterruptedException e) { Log.w(TAG, "Interrupted while waiting sound pool callback."); } } } else { status = -1; } if (mSoundPoolLooper != null) { mSoundPoolLooper.quit(); mSoundPoolLooper = null; } mSoundPoolListenerThread = null; if (status != 0) { Log.w(TAG, "onLoadSoundEffects(), Error "+status+ " while loading samples"); for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) { if (SOUND_EFFECT_FILES_MAP[effect][1] > 0) { SOUND_EFFECT_FILES_MAP[effect][1] = -1; } } mSoundPool.release(); mSoundPool = null; } } return (status == 0); } private String getSoundEffectFilePath(int effectType) { String filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); if (!new File(filePath).isFile()) { filePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH + SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]); } Log.d(TAG, "SoundEffectFilePath is : "+filePath); return filePath; }
代码中难懂的部分基本上都有注释,核心其实就是为SoundPool的load方法准备参数,其中有些数组嵌套的部分比较绕,但是只要把数组都写出来就一目了然了
onLoadSoundEffects基本上就是loadSoundEffects的全部内容,最后再来看一下onPlaySoundEffect的剩余部分
private void onPlaySoundEffect(int effectType, int volume) { synchronized (mSoundEffectsLock) { 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) { //调用SoundPool的play方法 mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1], volFloat, volFloat, 0, 0, 1.0f); Log.w(TAG, "Touch tone played"); } else { MediaPlayer mediaPlayer = new MediaPlayer(); try { String filePath = getSoundEffectFilePath(effectType); 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的play方法,其中传入了音量大小和之前load返回的sampleId
二、替换原生按键音
摘要:替换原生按键音的主要思路是:在初始化的时候在相关数组中增加自己自定义的音频资源,为了达到这个目的需要在文件中增加一些代表自己文件资源的常量,具体在哪个文件中增加,其实完全可以在熟悉源码流程之后模仿源码来增加;之后在播放按键音的时候主动调用自己的按键音资源就可以了;最后当然别忘了把音频文件push到设备中去。
需要修改的文件如下:
/frameworks/base/media/java/android/media/AudioManager.java
需要增加自己的音频种类,起名为:FX_KEYPRESS_CUSTOM,并把最大音频数量修改为11
/** * Invalid keypress sound * @see #playSoundEffect(int) */ public static final int FX_KEYPRESS_INVALID = 9; /** * @hide Custom sound * @see #playSoundEffect(int) */ public static final int FX_KEYPRESS_CUSTOM = 10; /** * @hide Number of sound effects */ public static final int NUM_SOUND_EFFECTS = 11;
需要注意的是自己增加的常量最好全部hide标记,这样可以免去执行make update-api指令,同时并不会影响使用,之后的修改都会遵循这一原则
/frameworks/base/core/java/android/view/SoundEffectConstants.java
同样需要增加一个常量:
public static final int CLICK = 0; public static final int NAVIGATION_LEFT = 1; public static final int NAVIGATION_UP = 2; public static final int NAVIGATION_RIGHT = 3; public static final int NAVIGATION_DOWN = 4; /** * @hide Custom click sound */ public static final int CLICK_CUSTOM = 5;
/frameworks/base/core/res/res/xml/audio_assets.xml
在XML文件中增加一个自己的音频文件,注意id和之前在AudioManager.java中增加的常量一致,file和push到设备中的文件名保持一致
<audio_assets version="1.0"> <group name="touch_sounds"> <asset id="FX_KEY_CLICK" file="Effect_Tick.ogg"/> <asset id="FX_FOCUS_NAVIGATION_UP" file="Effect_Tick.ogg"/> <asset id="FX_FOCUS_NAVIGATION_DOWN" file="Effect_Tick.ogg"/> <asset id="FX_FOCUS_NAVIGATION_LEFT" file="Effect_Tick.ogg"/> <asset id="FX_FOCUS_NAVIGATION_RIGHT" file="Effect_Tick.ogg"/> <asset id="FX_KEYPRESS_STANDARD" file="KeypressStandard.ogg"/> <asset id="FX_KEYPRESS_SPACEBAR" file="KeypressSpacebar.ogg"/> <asset id="FX_KEYPRESS_DELETE" file="KeypressDelete.ogg"/> <asset id="FX_KEYPRESS_RETURN" file="KeypressReturn.ogg"/> <asset id="FX_KEYPRESS_INVALID" file="KeypressInvalid.ogg"/> <asset id="FX_KEYPRESS_CUSTOM" file="boom.ogg"/> group>audio_assets>
准备工作已经完成了,现在来修改一下调用流程,主动调用自己的按键音
/frameworks/base/core/java/android/view/View.java
调用playSoundEffect时传入之前增加的常量:
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK_CUSTOM);//修改这里 li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
/frameworks/base/core/java/android/view/ViewRootImpl.java
View.java之后会调用到ViewRootImpl.java中,在switch/case中加入我们自己的情况:
@Override public void playSoundEffect(int effectId) { checkThread(); Log.d(mTag, "playSoundEffect"); 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; //增加的case语句 case SoundEffectConstants.CLICK_CUSTOM: audioManager.playSoundEffect(AudioManager.FX_KEYPRESS_CUSTOM); Log.d(mTag, "play my SoundEffect"); 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(); } }
以上就是修改的全部文件了,实际上只有5个文件,比预想的要简单的多,这全都要归功于Android源码出色的设计模式使其在代码上高度解耦
别忘了把音频文件push到设备里面,否则会启动异常的哦!push的路径为:/system/media/audio/ui/
可能有些小伙伴对于为什么要修改上面的文件有一些疑问,这里附上播放按键音的UML时序图,只要熟悉调用流程,就明白了
三、调节按键音音量方法
其实在之前的讲解过程中已经说到了,在调用SoundPool.play的时候其实会传入左右声道的音量值,只要按图索骥找到之前是在哪里传入的音量就可以啦!其实是在playSoundEffectVolume方法传入的音量值,那么只要在这个方法的参数中传入你想要的值就行了。
更多相关文章
- Android在Service服务中调用Activity活动的方法,实现UI界面更新操
- Android并发编程 多线程与锁
- Android卡顿相关原理和排查工具
- Android通知栏Notification的实现
- Android(安卓)WebView使用的技巧与一些坑
- Android入门——利用Canvas完成绘制点、圆、直线、路径、椭圆、
- Android学习―7种形式的Android(安卓)Dialog使用举例
- Android(安卓)or Linux 的休眠与唤醒
- Android(安卓)Unity3D 相互交互,及退出继承UnityPlayerActivity的