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

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

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

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图一:正常获取焦点

在看一下咱们的launcher的主页布局,由一层层ViewGroup嵌套而来,终究ShortcutAndWidgetContainer包括了桌面当前页面的一切使用图标

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图二:主页布局

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

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

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

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

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图三:BubbleTextView 可获取焦点日志

可见咱们的使用图标是具有获取焦点能力的。那究竟是什么阻止了KeyEvent派发到使用图标呢?由于keyEvent、和MotionEvent都是InputEvent的子类,那么感觉keyEvent的派发应该和MotionEvent差不多。经过检查源码,初次第一个焦点的检索如下流程所示:

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图四:退出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时分的调用栈,其间是检索阶段

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图五:检索默认焦点堆栈

下图展现了KeyEvent下发的调用栈,能够看到绑定了焦点途径的一切view或者viewGroup的DispatchKeyEvent都被调用了

Android模拟器外接键盘--剖析KeyEvent的寻焦与分配
图六:自上而下传递KeyEvent,终究被view接收处理

经过检查源码和触发调用栈,咱们清楚了在初次方向键触发的时分,其实会有一个寻焦过程,其由ViewRootImpl.EarlyPostImeInputStage.java接受,经过自上而下requestFocus循环调用终究确定了获取焦点的View,在之后触发的KeyEvent事情会根据“焦点途径”直接下发到焦点view中。

以上场景仅仅适用于初次寻焦,之后的KeyEvent派发都会由咱们自己代码进行焦点指定的场景。

下面放一张网上的图看下正常KeyEvent分配的流程

Android模拟器外接键盘--剖析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…