前言

记录一下Android按键音相关分析。

记录于此,方便自己查阅和回顾。

Android P源码上分析

正文

当一个View设置点击监听事件setOnClickListener()时,如果用户点击默认就会有按键音,当然如果你不需要,可以通过如下取消。

  1. xml配置

复制
  1. 指定View关闭

  1. 关闭Android所有VIew的按键音

  1. //0是关闭,1是打开
  2. Settings.System.putInt(TestApp.getContext().getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 0);
复制

今天关注上面几种配置分析。

1和2的正对View来说,设置一个属性,点击时判断是否需要响应按键音;3则是在framework层进行判断,是否需要响应。

View.java

下面两个配置一样的原理

  1. android:soundEffectsEnabled="false"
  2. mView.setSoundEffectsEnabled(false);
复制

都给View新增一个标志位(SOUND_EFFECTS_ENABLED & )。我们这里以setSoundEffectsEnabled()进行分析。

  1. \frameworks\base\core\java\android\view\View.java
复制
setSoundEffectsEnabled()
  1. /**
  2. * View flag indicating whether this view should have sound effects enabled
  3. * for events such as clicking and touching.
  4. */
  5. public static final int SOUND_EFFECTS_ENABLED = 0x08000000;
  6. public void setSoundEffectsEnabled(boolean soundEffectsEnabled) {
  7. setFlags(soundEffectsEnabled ? SOUND_EFFECTS_ENABLED: 0, SOUND_EFFECTS_ENABLED);
  8. }
复制
setFlags()

改变mViewFlags的标志位

  1. void setFlags(int flags, int mask) {
  2. final boolean accessibilityEnabled =
  3. AccessibilityManager.getInstance(mContext).isEnabled();
  4. final boolean oldIncludeForAccessibility = accessibilityEnabled && includeForAccessibility();
  5. int old = mViewFlags;
  6. mViewFlags = (mViewFlags & ~mask) | (flags & mask);
  7. int changed = mViewFlags ^ old;
  8. if (changed == 0) {
  9. return;
  10. }
  11. //略....
  12. }
复制
onTouchEvent()
  1. public boolean onTouchEvent(MotionEvent event) {
  2.   final float x = event.getX();
  3.   final float y = event.getY();
  4.   final int viewFlags = mViewFlags;
  5.   final int action = event.getAction();
  6. //略
  7.   if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
  8.       switch (action) {
  9.           case MotionEvent.ACTION_UP:
  10.               mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
  11.               if ((viewFlags & TOOLTIP) == TOOLTIP) {
  12.                   handleTooltipUp();
  13.               }
  14.               if (!clickable) {
  15.                   removeTapCallback();
  16.                   removeLongPressCallback();
  17.                   mInContextButtonPress = false;
  18.                   mHasPerformedLongPress = false;
  19.                   mIgnoreNextUpEvent = false;
  20.                   break;
  21.               }
  22.               boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
  23.               if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
  24.                   boolean focusTaken = false;
  25.                   if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
  26.                       focusTaken = requestFocus();
  27.                   }
  28.                   if (prepressed) {
  29.                       setPressed(true, x, y);
  30.                   }
  31.                   if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
  32.                       removeLongPressCallback();
  33.                       if (!focusTaken) {
  34. //按键音流程
  35.                           if (mPerformClick == null) {
  36.                               mPerformClick = new PerformClick();
  37.                           }
  38.                           if (!post(mPerformClick)) {
  39.                               performClickInternal();
  40.                           }
  41.                       }
  42.                   }
  43.                   if (mUnsetPressedState == null) {
  44.                       mUnsetPressedState = new UnsetPressedState();
  45.                   }
  46.                   if (prepressed) {
  47.                       postDelayed(mUnsetPressedState,
  48.                               ViewConfiguration.getPressedStateDuration());
  49.                   } else if (!post(mUnsetPressedState)) {
  50.                       mUnsetPressedState.run();
  51.                   }
  52.                   removeTapCallback();
  53.               }
  54.               mIgnoreNextUpEvent = false;
  55.               break;
  56.           case MotionEvent.ACTION_DOWN:
  57.               //略
  58.               break;
  59.           case MotionEvent.ACTION_CANCEL:
  60.               //略
  61.               break;
  62.           case MotionEvent.ACTION_MOVE:
  63.               //略
  64.               break;
  65.       }
  66.       return true;
  67.   }
  68.   return false;
  69. }
复制

这里重点关注

  1. //初始化mPerformClick
  2. if (mPerformClick == null) {
  3.   mPerformClick = new PerformClick();
  4. }
  5. //执行按键音
  6. if (!post(mPerformClick)) {
  7.   performClickInternal();
  8. }
复制

PerformClick实现Runnable,调用时是执行performClickInternal()的

  1. private final class PerformClick implements Runnable {
  2.   @Override
  3.   public void run() {
  4.       recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
  5.       performClickInternal();
  6.   }
  7. }
复制
post()
  1. public boolean post(Runnable action) {
  2.   final AttachInfo attachInfo = mAttachInfo;
  3.   if (attachInfo != null) {
  4.       return attachInfo.mHandler.post(action);
  5.   }
  6.   getRunQueue().post(action);
  7.   return true;
  8. }
复制

如果返回true,表示执行了mPerformClick,否则进入前面的if条件中的performClickInternal()。

其实不论true还是false,最终都是performClickInternal(),只不过调佣的方式不一样而已。

performClickInternal()
  1. private boolean performClickInternal() {
  2.   return performClick();
  3. }
复制
performClick()
  1. public boolean performClick() {
  2.   notifyAutofillManagerOnClick();
  3.   final boolean result;
  4.   final ListenerInfo li = mListenerInfo;
  5.   //重点,需要按键音,这里小设置mOnClickListener监听
  6.   if (li != null && li.mOnClickListener != null) {
  7.   //播放按键音
  8.       playSoundEffect(SoundEffectConstants.CLICK);
  9.       li.mOnClickListener.onClick(this);
  10.       result = true;
  11.   } else {
  12.       result = false;
  13.   }
  14.   sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
  15.   notifyEnterOrExitForAutoFillIfNeeded(true);
  16.   return result;
  17. }
复制
playSoundEffect()
  1. public void playSoundEffect(int soundConstant) {
  2. if (mAttachInfo == null || mAttachInfo.mRootCallbacks == null || !isSoundEffectsEnabled()) {
  3.       return;
  4.   }
  5.   //重点
  6. mAttachInfo.mRootCallbacks.playSoundEffect(soundConstant);
  7. }
复制

如果mAttachInfo等前面条件满足(一般是不会为null的),那么是否响应按键音,就取决于有没有SOUND_EFFECTS_ENABLED标志,也就是isSoundEffectsEnabled()。

我们看setSoundEffectsEnabled()

  1. @ViewDebug.ExportedProperty
  2. public boolean isSoundEffectsEnabled() {
  3.   return SOUND_EFFECTS_ENABLED == (mViewFlags & SOUND_EFFECTS_ENABLED);
  4. }
复制

返回true或false全看mViewFlags ,也就回到我们上面设置了SOUND_EFFECTS_ENABLED标志。

到此,View中影响按键应的分析导致结束了。

下面分析mAttachInfo.mRootCallbacks.playSoundEffect()是如何调用到framework层的按键音。

AttachInfo类

回到mAttachInfo。

AttachInfo定义也是在View.java内部的。

重点关注构造函数中倒数第二个参数Callbacks effectPlayer,看一下playSoundEffect()在哪里实现了即可。

  1. final static class AttachInfo {
  2. //重点看哪里实现的
  3.   interface Callbacks {
  4.       void playSoundEffect(int effectId);
  5.       boolean performHapticFeedback(int effectId, boolean always);
  6.   }
  7. //略
  8.   AttachInfo(IWindowSession session, IWindow window, Display display,
  9.           ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
  10.           Context context) {
  11.       mSession = session;
  12.       mWindow = window;
  13.       mWindowToken = window.asBinder();
  14.       mDisplay = display;
  15.       mViewRootImpl = viewRootImpl;
  16.       mHandler = handler;
  17.       mRootCallbacks = effectPlayer;
  18.       mTreeObserver = new ViewTreeObserver(context);
  19.   }
  20. //略
  21. }
复制

看先看AttachInfo初始化的地方,查看了View代码就是调用dispatchAttachedToWindow()赋值的。

  1. void dispatchAttachedToWindow(AttachInfo info, int visibility) {
  2. //mAttachInfo这里赋值了,也就是从其他地方初始化传入的
  3.   mAttachInfo = info;
  4. if (mOverlay != null) {
  5.       mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
  6.   }
  7. //略
  8. }
复制

而dispatchAttachedToWindow()调用时在ViewRootImpl.java中performTraversals()调用的。

具体过程略过吧,不过度细节哈

ViewRootImpl.java

  1. frameworks\base\core\java\android\view\ViewRootImpl.java
复制
performTraversals()

在performTraversals()中,代码很多,这里只展示部分

  1. private void performTraversals() {
  2.   final View host = mView;
  3. //略
  4. //第一次
  5.   if (mFirst) {
  6.       //略
  7. //这里传入mAttachInfo
  8.       host.dispatchAttachedToWindow(mAttachInfo, 0);
  9.       mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
  10.       dispatchApplyInsets(host);
  11.   } else {
  12.       desiredWindowWidth = frame.width();
  13.       desiredWindowHeight = frame.height();
  14.       if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
  15.           mFullRedrawNeeded = true;
  16.           mLayoutRequested = true;
  17.           windowSizeMayChange = true;
  18.       }
  19.   }
  20.   //略
  21. }
复制

而mAttachInfo的初始化是在ViewRootImpl()构造函数中。

  1. public ViewRootImpl(Context context, Display display) {
  2.   //略
  3.   mFirst = true; // true for the first time the view is added
  4.   mAdded = false;
  5. //mAttachInfo初始化
  6.   mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
  7.           context);
  8.   //略
  9. }
复制

这里重点关注倒数第二个参数(Callbacks effectPlayer)的赋值,这里传入的是this,也就是ViewRootImpl实现了对应的接口。

playSoundEffect()

AttachInfo接口中playSoundEffect()方法在这里实现。

  1. @Override
  2. public void playSoundEffect(int effectId) {
  3.   checkThread();
  4.   try {
  5.       final AudioManager audioManager = getAudioManager();
  6.       switch (effectId) {
  7.           case SoundEffectConstants.CLICK:
  8.           //调用了audioManager
  9.               audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
  10.               return;
  11.           case SoundEffectConstants.NAVIGATION_DOWN:
  12.               audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
  13.               return;
  14.           case SoundEffectConstants.NAVIGATION_LEFT:
  15.               audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
  16.               return;
  17.           case SoundEffectConstants.NAVIGATION_RIGHT:
  18.               audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
  19.               return;
  20.           case SoundEffectConstants.NAVIGATION_UP:
  21.               audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP);
  22.               return;
  23.           default:
  24.               throw new IllegalArgumentException("unknown effect id " + effectId +
  25.                       " not defined in " + SoundEffectConstants.class.getCanonicalName());
  26.       }
  27.   } catch (IllegalStateException e) {
  28.       e.printStackTrace();
  29.   }
  30. }
复制

最终还是调用了Framework层的AudioManager

  1. audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
复制
getAudioManager()

audioManager的获取

  1. private AudioManager getAudioManager() {
  2.   if (mView == null) {
  3.       throw new IllegalStateException("getAudioManager called when there is no mView");
  4.   }
  5.   if (mAudioManager == null) {
  6.       mAudioManager = (AudioManager) mView.getContext().getSystemService(Context.AUDIO_SERVICE);
  7.   }
  8.   return mAudioManager;
  9. }
复制

AudioManager.java

  1. \frameworks\base\media\java\android\media\AudioManager.java
复制
playSoundEffect()

playSoundEffect()存在多个,但都差不多,我们只关注上面调用的。

  1. public static final int NUM_SOUND_EFFECTS = 10;
  2. public void playSoundEffect(int effectType) {
  3. 查看是否越界
  4.   if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) {
  5.       return;
  6.   }
  7.   //查看是否可以播放按键音。
  8.   if (!querySoundEffectsEnabled(Process.myUserHandle().getIdentifier())) {
  9.       return;
  10.   }
  11.   final IAudioService service = getService();
  12.   try {
  13.   //调用AudioService播放
  14.       service.playSoundEffect(effectType);
  15.   } catch (RemoteException e) {
  16.       throw e.rethrowFromSystemServer();
  17.   }
  18. }
复制
querySoundEffectsEnabled()
  1. private boolean querySoundEffectsEnabled(int user) {
  2. int effect = Settings.System.getIntForUser(getContext().getContentResolver(),
  3.           Settings.System.SOUND_EFFECTS_ENABLED, 0, user)
  4.         //0是关闭,1是打开
  5.   return effect != 0;
  6. }
复制

这里获取系统设置Settings.System.SOUND_EFFECTS_ENABLED中的配置。

也就是我们开头的方式3的配置,如果配置了0,就是关闭整个系统的按键音。

我们继续分析是如何播放按键音的。

  1. final IAudioService service = getService();
  2. try {
  3.   service.playSoundEffect(effectType);
  4. } catch (RemoteException e) {
  5.   throw e.rethrowFromSystemServer();
  6. }
  7. private static IAudioService getService()
  8. {
  9.   if (sService != null) {
  10.       return sService;
  11.   }
  12.   IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
  13.   sService = IAudioService.Stub.asInterface(b);
  14.   return sService;
  15. }
复制

调用的是AudioService.playSoundEffect()

AudioService.java

playSoundEffect()
  1. public void playSoundEffect(int effectType) {
  2. playSoundEffectVolume(effectType, -1.0f);
  3. }
复制
playSoundEffectVolume()
  1. public void playSoundEffectVolume(int effectType, float volume) {
  2. //判断系统是否静音
  3. if (isStreamMutedByRingerOrZenMode(STREAM_SYSTEM)) {
  4. return;
  5. }
  6. //再次判断effectType是否越界
  7. if (effectType >= AudioManager.NUM_SOUND_EFFECTS || effectType < 0) {
  8. Log.w(TAG, "AudioService effectType value " + effectType + " out of range");
  9. return;
  10. }
  11. //发送MSG_PLAY_SOUND_EFFECT
  12. MSG_PLAY_SOUND_EFFECT消息
  13. sendMsg(mAudioHandler, MSG_PLAY_SOUND_EFFECT
  14. MSG_PLAY_SOUND_EFFECT消息, SENDMSG_QUEUE,
  15. effectType, (int) (volume * 1000), null, 0);
  16. }
复制
handleMessage()
  1. @Override
  2. public void handleMessage(Message msg) {
  3. //略
  4. case MSG_PLAY_SOUND_EFFECT:
  5. onPlaySoundEffect(msg.arg1, msg.arg2);
  6. break;
  7. //略
  8. }
复制
onPlaySoundEffect()
  1. private void onPlaySoundEffect(int effectType, int volume) {
  2.   synchronized (mSoundEffectsLock) {
  3.   //初始化mSoundPool
  4.       onLoadSoundEffects();
  5.       if (mSoundPool == null) {
  6.           return;
  7.       }
  8.       float volFloat;
  9.       // use default if volume is not specified by caller
  10.       if (volume < 0) {
  11.           volFloat = (float)Math.pow(10, (float)sSoundEffectVolumeDb/20);
  12.       } else {
  13.           volFloat = volume / 1000.0f;
  14.       }
  15.       //播放按键音有两种方式
  16.       //一种是SoundPool,另外一种是MediaPlayer
  17.       //具体使用哪种,要看SOUND_EFFECT_FILES_MAP[effectType][1]的值
  18.       if (SOUND_EFFECT_FILES_MAP[effectType][1] > 0) {
  19.           mSoundPool.play(SOUND_EFFECT_FILES_MAP[effectType][1],
  20.                               volFloat, volFloat, 0, 0, 1.0f);
  21.       } else {
  22.           MediaPlayer mediaPlayer = new MediaPlayer();
  23.           try {
  24.               String filePath = getSoundEffectFilePath(effectType);
  25.               mediaPlayer.setDataSource(filePath);
  26.               mediaPlayer.setAudioStreamType(AudioSystem.STREAM_SYSTEM);
  27.               mediaPlayer.prepare();
  28.               mediaPlayer.setVolume(volFloat);
  29.               mediaPlayer.setOnCompletionListener(new OnCompletionListener() {
  30.                   public void onCompletion(MediaPlayer mp) {
  31.                       cleanupPlayer(mp);
  32.                   }
  33.               });
  34.               mediaPlayer.setOnErrorListener(new OnErrorListener() {
  35.                   public boolean onError(MediaPlayer mp, int what, int extra) {
  36.                       cleanupPlayer(mp);
  37.                       return true;
  38.                   }
  39.               });
  40.               mediaPlayer.start();
  41.           } catch (IOException ex) {
  42.               Log.w(TAG, "MediaPlayer IOException: "+ex);
  43.           } catch (IllegalArgumentException ex) {
  44.               Log.w(TAG, "MediaPlayer IllegalArgumentException: "+ex);
  45.           } catch (IllegalStateException ex) {
  46.               Log.w(TAG, "MediaPlayer IllegalStateException: "+ex);
  47.           }
  48.       }
  49.   }
  50. }
复制

至此,按键音跟踪就结束了。

参考文章

相关文章

暂无评论

none
暂无评论...