Android模拟器外接键盘--分析KeyEvent的寻焦与分派
作者:访客发布时间:2023-12-24分类:程序开发学习浏览:85
手柄方向键无效,分析KeyEvent的寻焦与分派
刚起身准备去上厕所,就被小金拦住了去路。小金说:“我这里有个手柄的bug,你看看是不是你的问题”。小金有理有据的说:“你看看的launcher在android6上完美兼容手柄,但是在android12上,完全不能用手柄控制切换应用选择,下压方向键无数次,没有一个应用能被选中”。我竟无法反驳-乖乖把bug单接过来,开始看~ 对于一个android仔来说,接触的最多就是TouchEvent事件了,这个手柄的方向键是触发了什么事件呢? 一时有点蒙
正常手柄方向键触发之后的效果,如下图:
图一:正常获取焦点
在看一下我们的launcher的首页布局,由一层层ViewGroup嵌套而来,最终ShortcutAndWidgetContainer包含了桌面当前页面的所有应用图标 图二:首页布局
咱都2023年了,先问一波chatgpt。问:"android中手柄方向键触发失效,怎么解?",回答如下:
检查焦点和事件分发:手柄方向键触发通常需要焦点和正确的事件分发。请确保您的视图(如Activity或Fragment)具有焦点,并且正确地处理了方向键事件。您可以使用以下方法来检查和处理焦点和事件:
在视图的XML布局文件中,确保您的视图元素具有`android:focusable="true"`和`android:focusableInTouchMode="true"`属性,以确保能够获取焦点。
回答中提到了android:focusable、android:focusableInTouchMode、焦点等字眼,我们查看相关文档,知道一个view必须是可获取焦点的才可以分派keyEvent事件到该view。首先第一个问题出现,是否我们的应用图标在android12上是不可以获取焦点的呢?我们打印日志看看,如下图所示:
图三:BubbleTextView 可获取焦点日志
可见我们的应用图标是具备获取焦点能力的。那究竟是什么阻碍了KeyEvent派发到应用图标呢?因为keyEvent、和MotionEvent都是InputEvent的子类,那么感觉keyEvent的派发应该和MotionEvent差不多。通过查看源码,首次第一个焦点的检索如下流程所示: 图四:退出TouchMode下检索焦点和派发KeyEvent流程
其中涉及到的流程
// ViewRootImpl.EarlyPostImeInputStage.java
// 1.处理退出TouchMode入口
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else if (q.mEvent instanceof MotionEvent) {
return processMotionEvent(q);
}
return FORWARD;
}
// 2.退出touchmode处理
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent) q.mEvent;
// 省略 ....
// 判断退出触摸模式
if (checkForLeavingTouchModeAndConsume(event)) {
return FINISH_HANDLED;
}
// Make sure the fallback event policy sees all keys that will be
// delivered to the view hierarchy.
mFallbackEventHandler.preDispatchKeyEvent(event);
return FORWARD;
}
// 3.event事件具体类型检测,满足触发ensureTouchMode(false)
private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
// 省略 ....
// 1.因为我们触发的是←键,所以满足isNavigationKey判断
// If the key can be used for keyboard navigation then leave touch mode
// and select a focused view if needed (in ensureTouchMode).
// When a new focused view is selected, we consume the navigation key because
// navigation doesn't make much sense unless a view already has focus so
// the key's purpose is to set focus.
if (isNavigationKey(event)) {
return ensureTouchMode(false);
}
// If the key can be used for typing then leave touch mode
// and select a focused view if needed (in ensureTouchMode).
// Always allow the view to process the typing key.
if (isTypingKey(event)) {
ensureTouchMode(false);
return false;
}
return false;
}
// 4.inTouchMode = false,退出触摸模式
boolean ensureTouchMode(boolean inTouchMode) {
// .... 省略
// handle the change
// 继续处理
return ensureTouchModeLocally(inTouchMode);
}
// 5.inTouchMode = false,会执行enterTouchMode()
private boolean ensureTouchModeLocally(boolean inTouchMode) {
// .... 省略
return (inTouchMode) ? enterTouchMode() : leaveTouchMode();
}
//6.实际执行退出touchmode
private boolean leaveTouchMode() {
if (mView != null) {
// 1.首次没有可获取焦点view
if (mView.hasFocus()) {
View focusedView = mView.findFocus();
if (!(focusedView instanceof ViewGroup)) {
// some view has focus, let it keep it
return false;
} else if (((ViewGroup) focusedView).getDescendantFocusability() !=
ViewGroup.FOCUS_AFTER_DESCENDANTS) {
// some view group has focus, and doesn't prefer its children
// over itself for focus, so let them keep it.
return false;
}
}
// find the best view to give focus to in this brave new non-touch-mode
// world
// 2.获取默认焦点
return mView.restoreDefaultFocus();
}
return false;
}
// 7.触发实际检索焦点的逻辑,开启自上而下遍历寻焦
public boolean restoreDefaultFocus() {
return requestFocus(View.FOCUS_DOWN);
}
// 8.自上而下请求焦点
public final boolean requestFocus(int direction) {
return requestFocus(direction, null);
}
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
// 8.正常逻辑都是寻焦模式是以子view优先的,一些viewGroup拦截焦点逻辑的除外,比如配置了setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);等
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
result = super.requestFocus(direction, previouslyFocusedRect);
break;
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
result = took ? took : onRequestFocusInDescendants(direction,
previouslyFocusedRect);
break;
}
case FOCUS_AFTER_DESCENDANTS: {
// 1.主要是这里进焦点查找
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
break;
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
}
return result;
}
// 9.viewGroup的默认实现,这里深度优先遍历子view
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
index = 0;
increment = 1;
end = count;
} else {
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
// 子view调用请求焦点
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
// 10.上面requestFocus会调用requestFocusNoSearch,如何获取焦点就会执行handleFocusGainInternal,执行结束焦点查询
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if (!canTakeFocus()) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
// 1.终结焦点查询实际处理
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
// 11.其内部调用requestChildFocus自下而上更新所有viewgroup中的focused属性
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
// 我们这里得到了能处理焦点的view,现在自下而上更新所有viewgroup中的focused属性,绑定焦点下发链路
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
// 12.其内部调用mParent.requestChildFocus(this, focused)自下而上绑定焦点view路径
@Override
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
// 这里绑定直接包含获取焦点的子view到自己的focused属性
mParent.requestChildFocus(this, focused);
}
}
// 13.绑定焦点路径之后,会ViewRootImpl.ViewPostImeInputStage.java,实际的keyEvent派发
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
// 派发
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
// 14. 触发dispatchKeyEvent派发KeyEvent
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
if (mUnhandledKeyManager.preViewDispatch(event)) {
return FINISH_HANDLED;
}
// Deliver the key to the view hierarchy.
// 派发KeyEvent到View树中
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return 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.
// 省略 .....
return FORWARD;
}
// 15.嵌套的ViewGroup一层一层向下执行调用,ViewGroup.dispatchKeyEvent
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
// 这里mFocused是我们检索焦点完成之后,保存的直接包含焦点view的直接子类,这里循环下发到最终的有焦点的view上
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
// 16.最终获取焦点的view,执行其View.dispatchKeyEvent函数,判断是否有mOnKeyListener 触发,消费事件
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
下图中展示了检索焦点view时候的调用栈,其中是检索阶段
图五:检索默认焦点堆栈
下图展示了KeyEvent下发的调用栈,可以看到绑定了焦点路径的所有view或者viewGroup的DispatchKeyEvent都被调用了
图六:自上而下传递KeyEvent,最终被view接收处理
通过查看源码和触发调用栈,我们清楚了在首次方向键触发的时候,其实会有一个寻焦过程,其由ViewRootImpl.EarlyPostImeInputStage.java承接,通过自上而下requestFocus循环调用最终确定了获取焦点的View,在之后触发的KeyEvent事件会根据“焦点路径”直接下发到焦点view中。
以上场景只是适用于首次寻焦,之后的KeyEvent派发都会由我们自己代码进行焦点指定的场景。
下面放一张网上的图看下正常KeyEvent分派的流程
那么我们在android12 上遇到无法触发应用选中的问题要怎么定位呢?
首先看下焦点是否已经分配到目标view上,通过设置断点主要是断点到requestFocus函数。其中ViewGroup和View对其实现是不一致的,在ViewGroup中会通过descendantFocusability来决定判断策略,如果是以自己优先还是以子view优先,通过这一步,其实我们的问题已经可以得到答案,在首页架构的Celllayout层,其寻焦策略是FOCUS_BEFORE_DESCENDANTS,使得其子view没办法参与焦点的获取。我们修改其策略就可以解决。
参考资料:
blog.csdn.net/txksnail/ar…
www.jianshu.com/p/2115b3f17…
juejin.cn/post/684490…
www.cnblogs.com/tiantianbyc…
juejin.cn/post/727421…
juejin.cn/post/689555…
juejin.cn/post/727421…
juejin.cn/post/684490…
juejin.cn/post/727421…
juejin.cn/post/698919…
相关推荐
- 轻松上手:
(三)笔记可再编辑 - 如何在iPhone,iPad和Android上使用WordPress应用程序
- 一款简单高效的Android异步框架
- [Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- Android---View中的setMinWidth与setMinimumWidth的踩坑记录
- Android广播如何解决Sending non-protected broadcast问题
- 有关Android Binder面试,你未知的9个秘密
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- Android低功耗蓝牙开发总结
- Android 通知文本颜色获取
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版