目录
前言
记录一下Android按键音相关分析。
记录于此,方便自己查阅和回顾。
Android P源码上分析
正文
当一个View设置点击监听事件setOnClickListener()时,如果用户点击默认就会有按键音,当然如果你不需要,可以通过如下取消。
xml配置
- android:soundEffectsEnabled="false"
指定View关闭
- mView.setSoundEffectsEnabled(false);
关闭Android所有VIew的按键音
- //0是关闭,1是打开
- Settings.System.putInt(TestApp.getContext().getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 0);
今天关注上面几种配置分析。
1和2的正对View来说,设置一个属性,点击时判断是否需要响应按键音;3则是在framework层进行判断,是否需要响应。
View.java
下面两个配置一样的原理
- android:soundEffectsEnabled="false"
- mView.setSoundEffectsEnabled(false);
都给View新增一个标志位(SOUND_EFFECTS_ENABLED & )。我们这里以setSoundEffectsEnabled()进行分析。
- \frameworks\base\core\java\android\view\View.java
setSoundEffectsEnabled()
- /**
- * View flag indicating whether this view should have sound effects enabled
- * for events such as clicking and touching.
- */
- public static final int SOUND_EFFECTS_ENABLED = 0x08000000;
- public void setSoundEffectsEnabled(boolean soundEffectsEnabled) {
- setFlags(soundEffectsEnabled ? SOUND_EFFECTS_ENABLED: 0, SOUND_EFFECTS_ENABLED);
- }
setFlags()
改变mViewFlags的标志位
- void setFlags(int flags, int mask) {
- final boolean accessibilityEnabled =
- AccessibilityManager.getInstance(mContext).isEnabled();
- final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
- int old = mViewFlags;
- mViewFlags = (mViewFlags & ~mask) | (flags & mask);
- int changed = mViewFlags ^ old;
- if (changed == 0) {
- return;
- }
- //略....
- }
onTouchEvent()
- public boolean onTouchEvent(MotionEvent event) {
- final float x = event.getX();
- final float y = event.getY();
- final int viewFlags = mViewFlags;
- final int action = event.getAction();
- //略
- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
- switch (action) {
- case MotionEvent.ACTION_UP:
- mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
- if ((viewFlags & TOOLTIP) == TOOLTIP) {
- handleTooltipUp();
- }
- if (!clickable) {
- removeTapCallback();
- removeLongPressCallback();
- mInContextButtonPress = false;
- mHasPerformedLongPress = false;
- mIgnoreNextUpEvent = false;
- break;
- }
- boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
- if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
- boolean focusTaken = false;
- if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
- focusTaken = requestFocus();
- }
-
- if (prepressed) {
- setPressed(true, x, y);
- }
- if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
- removeLongPressCallback();
- if (!focusTaken) {
- //按键音流程
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- if (!post(mPerformClick)) {
- performClickInternal();
- }
- }
- }
- if (mUnsetPressedState == null) {
- mUnsetPressedState = new UnsetPressedState();
- }
- if (prepressed) {
- postDelayed(mUnsetPressedState,
- ViewConfiguration.getPressedStateDuration());
- } else if (!post(mUnsetPressedState)) {
- mUnsetPressedState.run();
- }
- removeTapCallback();
- }
- mIgnoreNextUpEvent = false;
- break;
- case MotionEvent.ACTION_DOWN:
- //略
- break;
- case MotionEvent.ACTION_CANCEL:
- //略
- break;
- case MotionEvent.ACTION_MOVE:
- //略
- break;
- }
- return true;
- }
- return false;
- }
这里重点关注
- //初始化mPerformClick
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- //执行按键音
- if (!post(mPerformClick)) {
- performClickInternal();
- }
PerformClick实现Runnable,调用时是执行performClickInternal()的
- private final class PerformClick implements Runnable {
- @Override
- public void run() {
- recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
- performClickInternal();
- }
- }
post()
- public boolean post(Runnable action) {
- final AttachInfo attachInfo = mAttachInfo;
- if (attachInfo != null) {
- return attachInfo.mHandler.post(action);
- }
- getRunQueue().post(action);
- return true;
- }
如果返回true,表示执行了mPerformClick,否则进入前面的if条件中的performClickInternal()。
其实不论true还是false,最终都是performClickInternal(),只不过调佣的方式不一样而已。
performClickInternal()
- private boolean performClickInternal() {
- return performClick();
- }
performClick()
- public boolean performClick() {
- notifyAutofillManagerOnClick();
- final boolean result;
- final ListenerInfo li = mListenerInfo;
- //重点,需要按键音,这里小设置mOnClickListener监听
- 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;
- }
playSoundEffect()
- public void playSoundEffect(int soundConstant) {
- if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
- return;
- }
- //重点
- mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
- }
如果mAttachInfo等前面条件满足(一般是不会为null的),那么是否响应按键音,就取决于有没有SOUND_EFFECTS_ENABLED标志,也就是isSoundEffectsEnabled()。
我们看setSoundEffectsEnabled()
- @ViewDebug.ExportedProperty
- public boolean isSoundEffectsEnabled() {
- return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED);
- }
返回true或false全看mViewFlags ,也就回到我们上面设置了SOUND_EFFECTS_ENABLED标志。
到此,View中影响按键应的分析导致结束了。
下面分析mAttachInfo.mRootCallbacks.playSoundEffect()是如何调用到framework层的按键音。
AttachInfo类
回到mAttachInfo。
AttachInfo定义也是在View.java内部的。
重点关注构造函数中倒数第二个参数Callbacks effectPlayer,看一下playSoundEffect()在哪里实现了即可。
- final static class AttachInfo {
- //重点看哪里实现的
- interface Callbacks {
- void playSoundEffect(int effectId);
- boolean performHapticFeedback(int effectId, boolean always);
- }
- //略
- 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;
- mTreeObserver = new ViewTreeObserver(context);
- }
- //略
- }
看先看AttachInfo初始化的地方,查看了View代码就是调用dispatchAttachedToWindow()赋值的。
- void dispatchAttachedToWindow(AttachInfo info, int visibility) {
- //mAttachInfo这里赋值了,也就是从其他地方初始化传入的
- mAttachInfo = info;
- if (mOverlay != null) {
- mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
- }
- //略
- }
而dispatchAttachedToWindow()调用时在ViewRootImpl.java中performTraversals()调用的。
具体过程略过吧,不过度细节哈
ViewRootImpl.java
- frameworks\base\core\java\android\view\ViewRootImpl.java
performTraversals()
在performTraversals()中,代码很多,这里只展示部分
- private void performTraversals() {
- final View host = mView;
- //略
- //第一次
- if (mFirst) {
- //略
- //这里传入mAttachInfo
- host.dispatchAttachedToWindow(mAttachInfo, 0);
- mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
- dispatchApplyInsets(host);
- } else {
- desiredWindowWidth = frame.width();
- desiredWindowHeight = frame.height();
- if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
- mFullRedrawNeeded = true;
- mLayoutRequested = true;
- windowSizeMayChange = true;
- }
- }
- //略
- }
而mAttachInfo的初始化是在ViewRootImpl()构造函数中。
- public ViewRootImpl(Context context, Display display) {
- //略
- mFirst = true; // true for the first time the view is added
- mAdded = false;
- //mAttachInfo初始化
- mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
- context);
- //略
- }
这里重点关注倒数第二个参数(Callbacks effectPlayer)的赋值,这里传入的是this,也就是ViewRootImpl实现了对应的接口。
playSoundEffect()
AttachInfo接口中playSoundEffect()方法在这里实现。
- @Override
- public void playSoundEffect(int effectId) {
- checkThread();
- try {
- final AudioManager audioManager = getAudioManager();
- switch (effectId) {
- case SoundEffectConstants.CLICK:
- //调用了audioManager
- 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) {
- e.printStackTrace();
- }
- }
最终还是调用了Framework层的AudioManager
- audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
getAudioManager()
audioManager的获取
- private AudioManager getAudioManager() {
- if (mView == null) {
- throw new IllegalStateException("getAudioManager called when there is no mView");
- }
- if (mAudioManager == null) {
- mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
- }
- return mAudioManager;
- }
AudioManager.java
- \frameworks\base\media\java\android\media\AudioManager.java
playSoundEffect()
playSoundEffect()存在多个,但都差不多,我们只关注上面调用的。
- public static final int NUM_SOUND_EFFECTS = 10;
-
- public void playSoundEffect(int effectType) {
- 查看是否越界
- if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
- return;
- }
- //查看是否可以播放按键音。
- if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
- return;
- }
- final IAudioService service = getService();
- try {
- //调用AudioService播放
- service.playSoundEffect(effectType);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
querySoundEffectsEnabled()
- private boolean querySoundEffectsEnabled(int user) {
- int effect = Settings.System.getIntForUser(getContext().getContentResolver(),
- Settings.System.SOUND_EFFECTS_ENABLED, 0, user)
- //0是关闭,1是打开
- return effect != 0;
- }
这里获取系统设置Settings.System.SOUND_EFFECTS_ENABLED中的配置。
也就是我们开头的方式3的配置,如果配置了0,就是关闭整个系统的按键音。
我们继续分析是如何播放按键音的。
- final IAudioService service = getService();
- try {
- service.playSoundEffect(effectType);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
-
- private static IAudioService getService()
- {
- if (sService != null) {
- return sService;
- }
- IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
- sService = IAudioService.Stub.asInterface(b);
- return sService;
- }
调用的是AudioService.playSoundEffect()
AudioService.java
playSoundEffect()
- public void playSoundEffect(int effectType) {
- playSoundEffectVolume(effectType, -1.0f);
- }
playSoundEffectVolume()
- public void playSoundEffectVolume(int effectType, float volume) {
- //判断系统是否静音
- if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
- return;
- }
- //再次判断effectType是否越界
- if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
- Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
- return;
- }
- //发送MSG_PLAY_SOUND_EFFECT
- MSG_PLAY_SOUND_EFFECT消息
- sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT
- 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;
- //略
- }
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;
- }
- //播放按键音有两种方式
- //一种是SoundPool,另外一种是MediaPlayer
- //具体使用哪种,要看SOUND_EFFECT_FILES_MAP[effectType][1]的值
- if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
- mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
- volFloat, volFloat, 0, 0, 1.0f);
- } 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);
- }
- }
- }
- }
至此,按键音跟踪就结束了。
参考文章
历史上的今天
- 《Android按键音简单源码分析》
- 《WordPress常用的函数介绍》
- 《他导航》
- 《搜狗明医》
- 《NavsMap》
- 《萌导航网》
- 《119下载站》
- 《西瓜分享》
- 《瓜子西瓜》