目录
前言
记录一下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); }
这里先看isSoundEffectsEnabled()简单的带过,如果mAttachInfo等前面条件满足,那么是否响应按键音,就取决于有没有SOUND_EFFECTS_ENABLED标志。
这就是setSoundEffectsEnabled()设置true和false影响的地方。
@ViewDebug.ExportedProperty public boolean isSoundEffectsEnabled() { return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED); }
致辞View中影响按键应的分析导致结束了。
下面分析mAttachInfo.mRootCallbacks.playSoundEffect()如何调用到framework层的按键音。
AttachInfo类
回到mAttachInfo。AttachInfo定义也是在View.java内部的。
这里构造函数中倒数第二个参数Callbacks effectPlayer。
void dispatchAttachedToWindow(AttachInfo info, int visibility) { //这有个接口,重点。上面就是通过接口调用的。 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初始化的地方。
void dispatchAttachedToWindow(AttachInfo info, int visibility) { //这里赋值了 mAttachInfo = info; if (mOverlay != null) { mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility); } //略 }
查看了View代码就是调用dispatchAttachedToWindow()赋值的。
而dispatchAttachedToWindow()调用时在ViewRootImpl.java中。
直接到了ViewRootImpl,具体暂时略过
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); } } } }
至此,按键音跟踪就结束了。
参考文章