用遥控器进行界面的tab切换时,会有音量出现,梳理下tab切换时按键音的逻辑。基于Android U的代码。
从ViewRootImpl的processKeyEvent方法开始,这个方法用于处理按键事件。首先看下该方法的调用堆栈。
at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:6957)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:6826)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6244)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6432)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6275)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:6489)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6248)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:6275)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:6248)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:6301)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:6267)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:6465)
at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:6676)
at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:4278)
at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:3715)
at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:3706)
at android.view.inputmethod.InputMethodManager.-$$Nest$mfinishedInputEvent(Unknown Source:0)
at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:4255)
at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:154)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loopOnce(Looper.java:162)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:570)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
源码分析:
frameworks/base/core/java/android/view/ViewRootImpl.java
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
if (mUnhandledKeyManager.preViewDispatch(event)) {//在event事件被分发给view层级结构之前调用,如果未处理的handler具有焦点并且消费了该事件,则返回true
return FINISH_HANDLED;//FINISH_HANDLED表示该事件已被处理。
}
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {//将key事件传递给mView(Activity的顶层DecordView对象)处理
return FINISH_HANDLED;//如果mView层级结构中任何一个view处理了该事件,返回FINISH_HANDLED表示该事件已被处理
}
if (shouldDropInputEvent(q)) {//是否应该放弃处理该事件
return FINISH_NOT_HANDLED;//返回FINISH_NOT_HANDLED表示该事件没有被处理
}
// This dispatch is for windows that don't have a Window.Callback. Otherwise,
// the Window.Callback usually will have already called this (see
// DecorView.superDispatchKeyEvent) leaving this call a no-op.
if (mUnhandledKeyManager.dispatch(mView, event)) {//是否有未处理的事件
return FINISH_HANDLED;//表示该事件已经被处理
}
int groupNavigationDirection = 0;
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getKeyCode() == KeyEvent.KEYCODE_TAB) {//tab键处理
if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_CTRL_ON)) {
groupNavigationDirection = View.FOCUS_FORWARD;//将焦点移至下一个可选的item
} else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
groupNavigationDirection = View.FOCUS_BACKWARD;//将焦点移至上一个可选的item
}
}
// If a modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())
&& groupNavigationDirection == 0) {//如果是快捷键
if (mView.dispatchKeyShortcutEvent(event)) {//如果mView处理了快捷键
return FINISH_HANDLED;//表示该事件已被处理
}
if (shouldDropInputEvent(q)) {//该快捷键是否应该被放弃处理
return FINISH_NOT_HANDLED;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {//下发给PhoneFallbackEventHandler处理该事件
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {//是否应该放弃处理该事件
return FINISH_NOT_HANDLED;
}
// Handle automatic focus changes.
//处理焦点变化,这里会进行按键音的处理
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {//
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
if (performFocusNavigation(event)) {//处理焦点变化
return FINISH_HANDLED;
}
}
}
return FORWARD;//如果到这里事件仍未被处理,返回FORWARD表示该事件应该被传递给下一个事件处理流程
}
大致流程:
1.在event事件被分发到mView之前,调用mUnhandledKeyManager.preViewDispatch(event)判断是否有未处理按键事件,如果有返回FINISH_HANDLED表示该事件已被处理。
boolean preViewDispatch(KeyEvent event) {
mDispatched = false;
if (mCurrentReceiver == null) {//mCurrentReceiver当前接收器。此值是瞬态的,在预分派和预览阶段之间使用,以确保其他输入阶段不会干扰跟踪。
mCurrentReceiver = mCapturedKeys.get(event.getKeyCode());
}
if (mCurrentReceiver != null) {//如果存在mCurrentReceiver
View target = mCurrentReceiver.get();
if (event.getAction() == KeyEvent.ACTION_UP) {
mCurrentReceiver = null;
}
if (target != null && target.isAttachedToWindow()) {
target.onUnhandledKeyEvent(event);
}
// consume anyways so that we don't feed uncaptured key events to other views
return true;//到这里的话,event事件无论如何都会消耗,返回true表示事件已被处理,这样就不会将未捕获的键事件提供给其他视图
}
return false;
}
2.调用mView.dispatchKeyEvent()方法将event事件传递给mView(Activity的顶层DecordView对象)处理,如果view层级结构中任何一个view处理了该事件,返回FINISH_HANDLED表示该事件已被处理。最主要的就是这部分,大部分按键都会被mView.dispatchKeyEvent()处理。
3.判断是否应该放弃处理该事件,如果放弃处理,那么返回FINISH_NOT_HANDLED表示该事件没有被处理。看下哪些场景应该放弃处理该事件
protected boolean shouldDropInputEvent(QueuedInputEvent q) {
if (mView == null || !mAdded) {//mView不存在
Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
return true;
}
// Find a reason for dropping or canceling the event.
final String reason;//放弃或取消处理该事件的reason
if (!mAttachInfo.mHasWindowFocus
&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
&& !isAutofillUiShowing()) {
// This is a non-pointer event and the window doesn't currently have input focus
// This could be an event that came back from the previous stage
// but the window has lost focus or stopped in the meantime.
reason = "no window focus";//window没有焦点
} else if (mStopped) {
reason = "window is stopped";//window处于stop状态
} else if (mIsAmbientMode
&& !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) {
reason = "non-button event in ambient mode";//ambient mode下没有button事件
} else if (mPausedForTransition && !isBack(q.mEvent)) {
reason = "paused for transition";//activity处于转换阶段,暂停处理event事件
} else {//大多会走这里,一般不会放弃处理event事件
// Most common path: no reason to drop or cancel the event
return false;
}
if (isTerminalInputEvent(q.mEvent)) {//不要删除终端输入事件,但将其标记为已取消
// Don't drop terminal input events, however mark them as canceled.
q.mEvent.cancel();
Slog.w(mTag, "Cancelling event (" + reason + "):" + q.mEvent);
return false;
}
// Drop non-terminal input events.//放弃处理非终端输入事件
Slog.w(mTag, "Dropping event (" + reason + "):" + q.mEvent);
return true;
}
4.如果事件仍未被处理,调用mUnhandledKeyManager.dispatch来判断是否有未处理的事件。如果有,则直接返回FINISH_HANDLED表示该事件已被处理。
5.Tab键和快捷键的处理。
6.如果事件仍未被处理,调用mFallbackEventHandler.dispatchKeyEvent(event)方法继续下发给PhoneFallbackEventHandler处理。
7.调用performFocusNavigation方法处理焦点变化逻辑
frameworks/base/core/java/android/view/ViewRootImpl.java
private boolean performFocusNavigation(KeyEvent event) {
int direction = 0;//焦点转换的方向
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT://左移
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT://右移
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP://上移
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN://下移
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB://tab键
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();//找到控件树中当前获取焦点的view
if (focused != null) {
View v = focused.focusSearch(direction);//根据当前获取焦点的view,在direction方向上找到下一个获取焦点的view
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {//旧焦点的移除,新焦点的建立
boolean isFastScrolling = event.getRepeatCount() > 0;
playSoundEffect(
SoundEffectConstants.getConstantForFocusDirection(direction,
isFastScrolling));//播放按键音的地方
return true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
主要流程:首先计算焦点转换的方向,然后在控件树中找到当前获取焦点的view,根据当前获取焦点的view,在direction方向上找到下一个获取焦点的view,最后移除旧焦点,建立新焦点,并播放按键音。
playSoundEffect()方法就是播放按键音的地方
public void playSoundEffect(@SoundEffectConstants.SoundEffect int effectId) {
if ((mDisplay.getFlags() & Display.FLAG_TOUCH_FEEDBACK_DISABLED) != 0) {
return;
}
checkThread();
try {
final AudioManager audioManager = getAudioManager();
if (mFastScrollSoundEffectsEnabled
&& SoundEffectConstants.isNavigationRepeat(effectId)) {
audioManager.playSoundEffect(
SoundEffectConstants.nextNavigationRepeatSoundEffectId());
return;
}
switch (effectId) {
case SoundEffectConstants.CLICK:
audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
return;
case SoundEffectConstants.NAVIGATION_DOWN:
case SoundEffectConstants.NAVIGATION_REPEAT_DOWN:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN);
return;
case SoundEffectConstants.NAVIGATION_LEFT:
case SoundEffectConstants.NAVIGATION_REPEAT_LEFT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT);
return;
case SoundEffectConstants.NAVIGATION_RIGHT:
case SoundEffectConstants.NAVIGATION_REPEAT_RIGHT:
audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT);
return;
case SoundEffectConstants.NAVIGATION_UP:
case SoundEffectConstants.NAVIGATION_REPEAT_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 null
Log.e(mTag, "FATAL EXCEPTION when attempting to play sound effect: " + e);
e.printStackTrace();
}
}
根据不同的direction方向播放不同的音效,最终调用AudioService.playSoundEffect()方法播放音效。传递的effectType参数值表示音效类型,在audio_assets.xml里配置:
<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_BACK" file="Effect_Tick.ogg"/>
可以通过修改这个配置或者对应的ogg文件来定制按键音效。
标签:java,ViewRootImpl,遥控器,Tab,按键,return,android,event,view From: https://blog.csdn.net/li_5033/article/details/140317209