联系我们
简单又实用的WordPress网站制作教学
当前位置:网站首页 > 程序开发学习 > 正文

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

作者:访客发布时间:2023-12-24分类:程序开发学习浏览:85


导读:手柄方向键无效,分析KeyEvent的寻焦与分派刚起身准备去上厕所,就被小金拦住了去路。小金说:“我这里有个手柄的bug,你看看是不是你的问题”。小金有理有据的说:“你看看的la...

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

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

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

image.png图一:正常获取焦点

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

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

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

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

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

image.png图三:BubbleTextView 可获取焦点日志

可见我们的应用图标是具备获取焦点能力的。那究竟是什么阻碍了KeyEvent派发到应用图标呢?因为keyEvent、和MotionEvent都是InputEvent的子类,那么感觉keyEvent的派发应该和MotionEvent差不多。通过查看源码,首次第一个焦点的检索如下流程所示: 焦点.png图四:退出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时候的调用栈,其中是检索阶段

image.png图五:检索默认焦点堆栈

下图展示了KeyEvent下发的调用栈,可以看到绑定了焦点路径的所有view或者viewGroup的DispatchKeyEvent都被调用了

image.png图六:自上而下传递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…


标签:模拟器外接键盘安卓系统KeyEvent


程序开发学习排行
最近发表
网站分类
标签列表