前言
当用户点击Android智能设备的按钮的时候,如果伴随有按键音效的话,会给用户更好的交互体验。本期我们将会结合Android系统源码来具体分析一下控件是如何发出按键音效的。
一、系统加载按键音效资源
1、在TV版的Android智能设备中,我们可以通过调节设置页面的开关来控制按键音效的有无,该设置页面对应的系统源码如下所示。
packages/apps/TvSettings/Settings/src/com/android/tv/settings/device/sound/SoundFragment.java
public class SoundFragment extends PreferenceControllerFragment implementsPreference.OnPreferenceChangeListener {private AudioManager mAudioManager;private Map<Integer, Boolean> mFormats;public static SoundFragment newInstance() {return new SoundFragment();}@Overridepublic void onAttach(Context context) {mAudioManager = context.getSystemService(AudioManager.class);mFormats = mAudioManager.getSurroundFormats();super.onAttach(context);}//用户的点击行为首先触发此方法@Overridepublic boolean onPreferenceTreeClick(Preference preference) {if (TextUtils.equals(preference.getKey(), KEY_SOUND_EFFECTS)) {final TwoStatePreference soundPref = (TwoStatePreference) preference;//调用setSoundEffectsEnabled来设置按键音的开启与关闭setSoundEffectsEnabled(soundPref.isChecked());}return super.onPreferenceTreeClick(preference);}//获取按键音效是否开启public static boolean getSoundEffectsEnabled(ContentResolver contentResolver) {return Settings.System.getInt(contentResolver, Settings.System.SOUND_EFFECTS_ENABLED, 1)!= 0;}//设置是否开启按键音效private void setSoundEffectsEnabled(boolean enabled) {if (enabled) {//如果开启按键音,则调用AudioManager的loadSoundEffects方法来加载按键音效资源mAudioManager.loadSoundEffects();} else {mAudioManager.unloadSoundEffects();}Settings.System.putInt(getActivity().getContentResolver(),Settings.System.SOUND_EFFECTS_ENABLED, enabled ? 1 : 0);}}
我们在设置页面点击按键音效开关按钮,最终会触发SoundFragment的setSoundEffectsEnabled方法,该方法会判断是否开启按键音,如果开启,则调用AudioManager的loadSoundEffects方法来加载按键音效资源,反之则会调用unloadSoundEffects方法不加载音效资源。
2、AudioManager的loadSoundEffects方法如下所示。
frameworks/base/media/java/android/media/AudioManager.java
public class AudioManager {//获取AudioService的代理对象private static IAudioService getService(){if (sService != null) {return sService;}IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);sService = IAudioService.Stub.asInterface(b);return sService;}public void loadSoundEffects() {final IAudioService service = getService();try {//调用AudioService服务的loadSoundEffects方法service.loadSoundEffects();} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}public void unloadSoundEffects() {final IAudioService service = getService();try {service.unloadSoundEffects();} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}
}
AudioManager的首先通过getService方法获取了音频服务AudioService的代理对象,然后调用该对象的具体方法。
3、AudioService的loadSoundEffects方法如下所示。
public class AudioService extends IAudioService.Stubimplements AccessibilityManager.TouchExplorationStateChangeListener,AccessibilityManager.AccessibilityServicesStateChangeListener {private AudioHandler mAudioHandler;//加载音效资源public boolean loadSoundEffects() {int attempts = 3;LoadSoundEffectReply reply = new LoadSoundEffectReply();synchronized (reply) {//调用sendMsg发送消息给mAudioHandler。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);}//不加载音效资源public void unloadSoundEffects() {sendMsg(mAudioHandler, MSG_UNLOAD_SOUND_EFFECTS, SENDMSG_QUEUE, 0, 0, null, 0);}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 expectedtime = mLastDeviceConnectMsgTime + 30;}mLastDeviceConnectMsgTime = time;}//调用handler的sendMessageAtTime方法handler.sendMessageAtTime(handler.obtainMessage(msg, arg1, arg2, obj), time);}}private class AudioHandler extends Handler {//加载音效private boolean onLoadSoundEffects() {...代码暂时省略...}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {...代码省略...case MSG_UNLOAD_SOUND_EFFECTS:onUnloadSoundEffects();//不加载音效break;case MSG_LOAD_SOUND_EFFECTS://加载音效boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loadedif (msg.obj != null) {LoadSoundEffectReply reply = (LoadSoundEffectReply)msg.obj;synchronized (reply) {reply.mStatus = loaded ? 0 : -1;reply.notify();}}break;...代码省略...}}
}
这里我们只要分析一下AudioService的加载音效资源的loadSoundEffects方法,该方法会调用sendMsg,发送类型为MSG_UNLOAD_SOUND_EFFECTS的msg给mAudioHandler。然后会进一步触发AudioHandler的handleMessage方法,该消息最终会触发onLoadSoundEffects方法。
4、AudioHandler的onLoadSoundEffects方法如下所示。
public class AudioService extends IAudioService.Stubimplements AccessibilityManager.TouchExplorationStateChangeListener,AccessibilityManager.AccessibilityServicesStateChangeListener {//音效资源文件名称private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();private class AudioHandler extends Handler {//加载音效private boolean onLoadSoundEffects() {int status;synchronized (mSoundEffectsLock) {//如果系统未启动完毕直接返回if (!mSystemReady) {Log.w(TAG, "onLoadSoundEffects() called before boot complete");return false;}//如果mSoundPool不为空直接返回if (mSoundPool != null) {return true;}//加载触摸音效loadTouchSoundAssets();...代码暂时省略...}//加载按键音效资源private void loadTouchSoundAssets() {XmlResourceParser parser = null;//如果音效资源文件列表不为空直接返回if (!SOUND_EFFECT_FILES.isEmpty()) {return;}//加载按键默认的音效资源loadTouchSoundAssetDefaults();...代码省略...}private void loadTouchSoundAssetDefaults() {//在类型为List<String>的SOUND_EFFECT_FILES中添加默认的按键音效资源Effect_Tick.oggSOUND_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;}}
}
onLoadSoundEffects方法首先判断系统是否已经启动完毕,如果未启动直接返回;然后判断mSoundPool是否空,如果不为空则直接返回;
然后首先会调用一个关键方法loadTouchSoundAssets,该方法首先判断音效资源文件列表SOUND_EFFECT_FILES是否为空,不为空直接返回。如果以上判断都不成立,则会调用loadTouchSoundAssetDefaults方法加载按键默认的音效资源,该方法首先在SOUND_EFFECT_FILES的添加音效资源Effect_Tick.ogg。
5、继续往下看AudioHandler的onLoadSoundEffects方法。
public class AudioService extends IAudioService.Stubimplements AccessibilityManager.TouchExplorationStateChangeListener,AccessibilityManager.AccessibilityServicesStateChangeListener {//音效资源文件名称private static final List<String> SOUND_EFFECT_FILES = new ArrayList<String>();private class AudioHandler extends Handler {//加载音效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();//创建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();...代码省略...int numSamples = 0;for (int effect = 0; effect < AudioManager.NUM_SOUND_EFFECTS; effect++) {// Do not load sample if this effect uses the MediaPlayerif (SOUND_EFFECT_FILES_MAP[effect][1] == 0) {continue;}if (poolId[SOUND_EFFECT_FILES_MAP[effect][0]] == -1) {//获取音效资源文件路径String filePath = getSoundEffectFilePath(effect);//使用SoundPool加载音效资源文件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]];}}...代码省略...}//获取音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.oggprivate String getSoundEffectFilePath(int effectType) {// /product + /media/audio/ui/ + Effect_Tick.oggString filePath = Environment.getProductDirectory() + SOUND_EFFECTS_PATH+ SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);if (!new File(filePath).isFile()) {// /system + /media/audio/ui/ + Effect_Tick.oggfilePath = Environment.getRootDirectory() + SOUND_EFFECTS_PATH+ SOUND_EFFECT_FILES.get(SOUND_EFFECT_FILES_MAP[effectType][0]);}return filePath;}}}
onLoadSoundEffects先是调用loadTouchSoundAssets方法加载默认的音效资源文件名称,然后构建SoundPool实例对象,随后调用getSoundEffectFilePath获取按键音效资源文件路径,默认返回的音效资源文件路径为/system/media/audio/ui/Effect_Tick.ogg,并调用SoundPool加载该音效资源。
6、简单回顾一下以上步骤。
二、点击控件,播放音效资源
在系统开启按键音效之后,当我们点击任意控件之后,都会触发按键音效。接下来我们将会结合View的系统源码来梳理该流程。
1、当我们点击一个控件的时候,首先会触发该View的performClick方法。
frameworks/base/core/java/android/view/View.java
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {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);//播放按键点击音效li.mOnClickListener.onClick(this);result = true;} else {result = false;}sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);notifyEnterOrExitForAutoFillIfNeeded(true);return result;}public void playSoundEffect(int soundConstant) {//判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {return;}//调用mAttachInfo.mRootCallbacks的playSoundEffect方法mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);}}
View的performClick方法会调用playSoundEffect方法,playSoundEffect方法首先判断mAttachInfo.mRootCallbacks是否为空,以及系统是否开启了按键音效,然后调用mAttachInfo.mRootCallbacks的playSoundEffect方法。我们知道WindowManager在将View添加到窗口的过程中,都需要用到ViewRootImpl这个类,具体请参考Android 9.0系统源码_窗口管理(二)WindowManager对窗口的管理过程这篇文章。
2、mAttachInfo最初是在ViewRootImpl的构造方法中被创建的。
frameworks/base/core/java/android/view/ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {public ViewRootImpl(Context context, Display display) {mContext = context;mWindowSession = WindowManagerGlobal.getWindowSession();mDisplay = display;mBasePackageName = context.getBasePackageName();mThread = Thread.currentThread();mLocation = new WindowLeaked(null);mLocation.fillInStackTrace();mWidth = -1;mHeight = -1;mDirty = new Rect();mTempRect = new Rect();mVisRect = new Rect();mWinFrame = new Rect();mWindow = new W(this);mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;mViewVisibility = View.GONE;mTransparentRegion = new Region();mPreviousTransparentRegion = new Region();mFirst = true; // true for the first time the view is addedmAdded = false;//创建AttachInfo对象,倒数第二个参数就是View的playSoundEffect方法所用到的回调对象mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context);...代码省略...}
}
3、看完ViewRootImpl的构造方法,再来看下AttachInfo的构造方法。
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {final static class AttachInfo {//关键回调接口interface Callbacks {//播放音效void playSoundEffect(int effectId);boolean performHapticFeedback(int effectId, boolean always);}final Callbacks mRootCallbacks;AttachInfo(IWindowSession session, IWindow window, Display display,ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,Context context) {mSession = session;mWindow = window;mWindowToken = window.asBinder();mDisplay = display;mViewRootImpl = viewRootImpl;mHandler = handler;mRootCallbacks = effectPlayer;//View的playSoundEffect方法所用到的回调对象就是这个mTreeObserver = new ViewTreeObserver(context);}}}
AttachInfo构造方法的最后一个参数很关键,因为View的playSoundEffect方法所调用的对象就是这个,结合ViewRootImpl的代码我们可以知道是ViewRootImpl实现了这个回调。
4、ViewRootImpl的playSoundEffect方法如下所示。
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {public void playSoundEffect(int effectId) {checkThread();//检测是否是UI线程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 nullLog.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);e.printStackTrace();}}
}
ViewRootImpl的playSoundEffect方法首先会检测一下当前线程是不是UI线程,然后会根据传入的effectId类型来判断要播放那种音效。因为View的performClick方法传入的是SoundEffectConstants.CLICK,所以会触发audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK)。
4、AudioManager的playSoundEffect方法如下所示。
public class AudioManager {public void playSoundEffect(int effectType) {//检测音效类型是否合规if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {return;}//确定音效是否可用if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {return;}//获取AudioService服务final IAudioService service = getService();try {//调用服务的playSoundEffect方法service.playSoundEffect(effectType);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}/*** Settings has an in memory cache, so this is fast.*/private boolean querySoundEffectsEnabled(int user) {return Settings.System.getIntForUser(getContext().getContentResolver(),Settings.System.SOUND_EFFECTS_ENABLED, 0, user) != 0;}
}
AudioManager的playSoundEffect会做一些校验,如果校验通过则会获取AudioService服务对象,并调用该对象的playSoundEffect方法进行音效播放。
5、AudioService和playSoundEffect相关的代码如下所示。
public class AudioService extends IAudioService.Stubimplements AccessibilityManager.TouchExplorationStateChangeListener,AccessibilityManager.AccessibilityServicesStateChangeListener {/*** 播放音效* @param effectType*/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 mutedif (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {return;}if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {Log.w(TAG, "AudioService effectType value " + effectType + " out of range");return;}//发送播放音效的消息给mAudioHandlersendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT, SENDMSG_QUEUE,effectType, (int) (volume * 1000), null, 0);}private class AudioHandler extends Handler {//加载音效private boolean onLoadSoundEffects() {...代码暂时省略...}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {...代码省略...case MSG_UNLOAD_SOUND_EFFECTS:onUnloadSoundEffects();//不加载音效break;case MSG_LOAD_SOUND_EFFECTS://加载音效boolean loaded = onLoadSoundEffects();//调用onLoadSoundEffects加载音效,并将加载结果赋值给loaded...代码省略...break;case MSG_PLAY_SOUND_EFFECT://播放音效onPlaySoundEffect(msg.arg1, msg.arg2);break;...代码省略...}}
}
AudioService的playSoundEffect方法进一步调用playSoundEffectVolume,该方法会发送播放音效的消息MSG_PLAY_SOUND_EFFECT给mAudioHandler,最终会触发onPlaySoundEffect方法。
6、AudioService的onPlaySoundEffec方法如下所示。
public class AudioService extends IAudioService.Stubimplements AccessibilityManager.TouchExplorationStateChangeListener,AccessibilityManager.AccessibilityServicesStateChangeListener {private void onPlaySoundEffect(int effectType, int volume) {synchronized (mSoundEffectsLock) {onLoadSoundEffects();//加载音效if (mSoundPool == null) {return;}float volFloat;// use default if volume is not specified by callerif (volume < 0) {volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);} else {volFloat = volume / 1000.0f;}if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {//通过SoundPool播放音效mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],volFloat, volFloat, 0, 0, 1.0f);} else {//通过MediaPlayer播放音效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);}}}}}
7、简单回顾一下以上步骤。