Android模拟器外接键盘--分析KeyEvent的寻焦与分派

手柄方向键无效,分析KeyEvent的寻焦与分派

刚起身准备去上厕所,就被小金拦住了去路。小金说:"我这里有个手柄的bug,你看看是不是你的问题"。小金有理有据的说:"你看看的launcher在android6上完美兼容手柄,但是在android12上,完全不能用手柄控制切换应用选择,下压方向键无数次,没有一个应用能被选中"。我竟无法反驳-乖乖把bug单接过来,开始看~ 对于一个android仔来说,接触的最多就是TouchEvent事件了,这个手柄的方向键是触发了什么事件呢? 一时有点蒙

正常手柄方向键触发之后的效果,如下图:

图一:正常获取焦点

在看一下我们的launcher的首页布局,由一层层ViewGroup嵌套而来,最终ShortcutAndWidgetContainer包含了桌面当前页面的所有应用图标 图二:首页布局

咱都2023年了,先问一波chatgpt。问:"android中手柄方向键触发失效,怎么解?",回答如下:

检查焦点和事件分发:手柄方向键触发通常需要焦点和正确的事件分发。请确保您的视图(如Activity或Fragment)具有焦点,并且正确地处理了方向键事件。您可以使用以下方法来检查和处理焦点和事件:

ini 复制代码
在视图的XML布局文件中,确保您的视图元素具有`android:focusable="true"`和`android:focusableInTouchMode="true"`属性,以确保能够获取焦点。

回答中提到了android:focusableandroid:focusableInTouchMode焦点等字眼,我们查看相关文档,知道一个view必须是可获取焦点的才可以分派keyEvent事件到该view。首先第一个问题出现,是否我们的应用图标在android12上是不可以获取焦点的呢?我们打印日志看看,如下图所示:

图三:BubbleTextView 可获取焦点日志

可见我们的应用图标是具备获取焦点能力的。那究竟是什么阻碍了KeyEvent派发到应用图标呢?因为keyEvent、和MotionEvent都是InputEvent的子类,那么感觉keyEvent的派发应该和MotionEvent差不多。通过查看源码,首次第一个焦点的检索如下流程所示: 图四:退出TouchMode下检索焦点和派发KeyEvent流程

其中涉及到的流程

java 复制代码
	// 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...

相关推荐
CYRUS_STUDIO32 分钟前
利用 Linux 信号机制(SIGTRAP)实现 Android 下的反调试
android·安全·逆向
CYRUS_STUDIO1 小时前
Android 反调试攻防实战:多重检测手段解析与内核级绕过方案
android·操作系统·逆向
黄林晴5 小时前
如何判断手机是否是纯血鸿蒙系统
android
火柴就是我5 小时前
flutter 之真手势冲突处理
android·flutter
法的空间5 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
循环不息优化不止5 小时前
深入解析安卓 Handle 机制
android
恋猫de小郭5 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
jctech6 小时前
这才是2025年的插件化!ComboLite 2.0:为Compose开发者带来极致“爽”感
android·开源
用户2018792831676 小时前
为何Handler的postDelayed不适合精准定时任务?
android
叽哥6 小时前
Kotlin学习第 8 课:Kotlin 进阶特性:简化代码与提升效率
android·java·kotlin