7【Android 12】输入事件在App层的分发流程(三) —— KeyEvent处理流程

在这里插入图片描述

7 KeyEvent事件发送流程

7.1 ViewRootImpl.ViewPostImeInputStage.processKeyEvent

        private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;

		   ......	

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                if (ViewDebugManager.DEBUG_ENG) {
                    Log.v(mTag, "App handle key event: event = " + event + ", mView = " + mView
                            + ", this = " + this);
                }
                return FINISH_HANDLED;
            }

		   ......	
        }

专注于KeyEvent是如何发送给View层级结构的,其他的暂时不关注。

这里的mView是View层级结构的根VIew,对于Activity来说就是DecorView。

7.2 DecorView.dispatchKeyEvent

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int action = event.getAction();
        final boolean isDown = action == KeyEvent.ACTION_DOWN;

		......

        if (!mWindow.isDestroyed()) {
            final Window.Callback cb = mWindow.getCallback();
            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                    : super.dispatchKeyEvent(event);
            if (handled) {
                return true;
            }
        }

        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
    }

这里的mWIndow即PhoneWindow,PhoneWIndow的callback是Activity.attach的时候通过PhoneWindow.setCallback设置的,传入的是Activity自己。

那么这里的逻辑是,先调用Activity.dispatchKeyEvent去处理KeyEvent,如果Activity能够处理,那么当前输入事件被认为处理完成。否则,调用PhoneWindow.onKeyDown或者PhoneWindow.onKeyUp处理。

7.3 Activity.dispatchKeyEvent

    /**
     * Called to process key events.  You can override this to intercept all
     * key events before they are dispatched to the window.  Be sure to call
     * this implementation for key events that should be handled normally.
     *
     * @param event The key event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchKeyEvent(KeyEvent event) {
        onUserInteraction();

        // Let action bars open menus in response to the menu key prioritized over
        // the window handling it
        final int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_MENU &&
                mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
            return true;
        }

        Window win = getWindow();
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

这个方法用来处理KeyEvent事件,你可以重写这个方法,从而在所有KeyEvent发送给Window之前将其拦截。

这个方法的主要内容有:

1)、如果当前KeyEvent对应KeyEvent.KEYCODE_MENU,那么尝试让ActionBar调用onMenuKeyEvent去处理此事件。

2)、调用PhoneWindow.superDispatchKeyEvent继续往View层级结构发送KeyEvent。

3)、如果经过上面两步,当前KeyEvent还没有被处理,那么调用Activity的onKeyDown、onKeyLongPress和onKeyUp等方法去处理当前事件。

主要分析第二步。

7.4 PhoneWindow.superDispatchKeyEvent

    @Override
    public boolean superDispatchKeyEvent(KeyEvent event) {
        return mDecor.superDispatchKeyEvent(event);
    }

mDecor成员变量是一个DecorView对象,那么这里调用的就是DecorView.superDispatchKeyEvent。

7.5 DeocrView.superDispatchKeyEvent

    public boolean superDispatchKeyEvent(KeyEvent event) {
		......

        if (super.dispatchKeyEvent(event)) {
            return true;
        }

        ......
    }

这里调用的是父类的dispatchKeyEvent方法,由于直接父类FrameLayout没有重写这个方法,因此调用的就是ViewGroup的方法。

7.6 ViewGroup.dispatchKeyEvent

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
		......

        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) {
			......
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }

		......
        return false;
    }

这里的内容很简单:

1)、如果当前ViewGroup的mPrivateFlags有PFLAG_FOCUSED和PFLAG_HAS_BOUNDS这两个标记,那么说明当前ViewGroup拥有焦点,并且已经设置了自身的区域,那么调用基类VIew.dispatchKeyEvent,表示当前KeyEvent将由当前ViewGroup处理,不会再分发给子View。

2)、如果mFocused不为NULL,且mFocused的mPrivateFlags有PFLAG_HAS_BOUNDS,那么将事件发送给mFocused去处理。如果mFocused重写了VIew.dispatchKeyEvent方法,那么就调用mFocused自己的VIew.dispatchKeyEvent方法,否则调用基类的VIew.dispatchKeyEvent。

在进一步分析VIew.dispatchKeyEvent之前,这里有几个和焦点相关的概念需要先弄懂是什么意思。

7.6.1 PFLAG_HAS_BOUNDS

PFLAG_HAS_BOUNDS同样是mPrivateFlags的标志位之一:

    /** {@hide} */
    static final int PFLAG_HAS_BOUNDS                  = 0x00000010;

PFLAG_HAS_BOUNDS唯一被添加到mPrivateFlags的地方在View.setFrame中,那么可以尝试去理解为什么在View.dispatchKeyEvent中要去判断PFLAG_HAS_BOUNDS的意义:如果PFLAG_HAS_BOUNDS没有设置,说明此时当前View还没有layout完成,自身的frame还没有设置,那么这个View是无法作为焦点View的存在去处理当前的KeyEvent的。

7.6.2 PFLAG_FOCUSED

PFLAG_FOCUSED定义在View中,作为View的mPrivateFlags成员变量的标志位:

    /** {@hide} */
    static final int PFLAG_FOCUSED                     = 0x00000002;

首先看几个判断PFLAG_FOCUSED的地方:

    /**
     * Returns true if this view has focus itself, or is the ancestor of the
     * view that has focus.
     *
     * @return True if this view has or contains focus, false otherwise.
     */
    @ViewDebug.ExportedProperty(category = "focus")
    public boolean hasFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }

    /**
     * Returns true if this view has focus
     *
     * @return True if this view has focus, false otherwise.
     */
    @ViewDebug.ExportedProperty(category = "focus")
    @InspectableProperty(hasAttributeId = false)
    public boolean isFocused() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }

hasFocus注释是说,如果某一个View的mPrivateFlags包含PFLAG_FOCUSED,说明当前View持有焦点,或者是持有焦点的View的祖先View。

isFocused注释是说,如果某一个View的mPrivateFlags包含PFLAG_FOCUSED,说明当前View持有焦点。

感觉这个两个方法的意义是有一点冲突。

7.6.3 View.handleFocusGainInternal

PFLAG_FOCUSED唯一添加到mPrivateFlags的地方在View.handleFocusGainInternal:

    /**
     * Give this view focus. This will cause
     * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called.
     *
     * Note: this does not check whether this {@link View} should get focus, it just
     * gives it focus no matter what.  It should only be called internally by framework
     * code that knows what it is doing, namely {@link #requestFocus(int, Rect)}.
     *
     * @param direction values are {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
     *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT}. This is the direction which
     *        focus moved when requestFocus() is called. It may not always
     *        apply, in which case use the default View.FOCUS_DOWN.
     * @param previouslyFocusedRect The rectangle of the view that had focus
     *        prior in this View's coordinate system.
     */
    void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG || ViewDebugManager.DEBUG_FOCUS) {
            System.out.println(this + " requestFocus()");
            Log.d(VIEW_LOG_TAG, "handleFocusGainInternal: this = " + this + ", callstack = " ,
                    new Throwable("ViewFocus"));
        }

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            mPrivateFlags |= PFLAG_FOCUSED;

            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

            if (mParent != null) {
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }

            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }

给当前View焦点。这将会导致View.onFocusChanged方法被调用。

注意:这里不会检查当前View是否应该取得焦点,只是把焦点给到这个View,无论如何。这个方法应该只被framwork内部的,知道当前正在发生什么的代码调用,即View.requestFocus方法。

从这里再来看7.6.2,感觉只是持有焦点的View的mPrivateFlags才会被添加PFLAG_FOCUSED标记,持有焦点的View的祖先View并不会添加。

这里除了注释中所说的View.onFocusChanged方法之外,还有一个重要的点:

mParent.requestChildFocus(this, this);

调用了ViewParent.requestChildFocus方法,这个后面会一起分析。

7.6.4 ViewGroup.mFocused

    // The view contained within this ViewGroup that has or contains focus.
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    private View mFocused;

当前VIewGroup中的持有或者包含焦点的View。

唯一赋值的地方在ViewGroup.requestChildFocus方法:

    @Override
    public void requestChildFocus(View child, View focused) {
        if (DBG || ViewDebugManager.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) {
            mParent.requestChildFocus(this, focused);
        }
    }

该方法重写自ViewParent.requestChildFocus:

    /**
     * Called when a child of this parent wants focus
     * 
     * @param child The child of this ViewParent that wants focus. This view
     *        will contain the focused view. It is not necessarily the view that
     *        actually has focus.
     * @param focused The view that is a descendant of child that actually has
     *        focus
     */
    public void requestChildFocus(View child, View focused);

在当前ViewParent的一个子View想要获取焦点的时候调用。

参数child代表当前ViewParent中的一个想要焦点的子View。这个子View将会包含焦点View,但是它不一定是实际持有焦点的那个View。

参数focused这里我理解的是,就是实际上那个持有焦点的View,是第一个参数child的后代View。

这里看到ViewGroup.requestChildFocus有两个重要作用,一个是将mFocused指向包含焦点的子View,另一个是递归调用父View的requestChildFocus方法。

7.6.5 焦点请求实际验证

这里写了一个demo App,层级结构是:

    View Hierarchy:
      DecorView@fce7e75[MainActivity]
        android.widget.LinearLayout{730e35f V.E...... ........ 0,0-1200,1824}
          android.view.ViewStub{6df4c47 G.E...... ......I. 0,0-0,0 #10201c4 android:id/action_mode_bar_stub}
          android.widget.FrameLayout{8f1ae0a V.E...... ........ 0,48-1200,1824 #1020002 android:id/content}
            com.test.inputinviewhierarchy.MyLayout{7aa567b V.E...... ........ 0,0-1200,1776}
              android.widget.EditText{6349195 VFED..CL. ........ 0,0-1200,200}
        android.view.View{5eeca44 V.ED..... ........ 0,1824-1200,1920 #1020030 android:id/navigationBarBackground}
        android.view.View{ce5072d V.ED..... ........ 0,0-1200,48 #102002f android:id/statusBarBackground}

简化掉不必要的部分:

    View Hierarchy:
      DecorView@fce7e75[MainActivity]
        android.widget.LinearLayout{730e35f V.E...... ........ 0,0-1200,1824}
          android.widget.FrameLayout{8f1ae0a V.E...... ........ 0,48-1200,1824 #1020002 android:id/content}
            com.test.inputinviewhierarchy.MyLayout{7aa567b V.E...... ........ 0,0-1200,1776}
              android.widget.EditText{6349195 VFED..CL. ........ 0,0-1200,200}

其中MyLayout是我自己写的Activity加载的布局结构,继承RelativeLayout,只包含了一个普通的EditText。

首先Activity启动后,EditText并不会请求焦点,但是如果我们用手指点击了EditText的相关区域,EditText就会去请求焦点:

在这里插入图片描述

这里看到EditText是在View.onTouchEvent中调用View.requestFocus去请求了焦点,最终会调用到View.handleFocusGainInternal中。并且整个过程中,只有EditText调用了handleFocusGainInternal方法,其他View并没有调用,这也印证了7.6.3的说法,只有持有焦点的View的mPrivateFlags才会被添加PFLAG_FOCUSED标记,持有焦点的View的祖先View并不会添加。

根据7.7.3,当EditText的mPrivateFlags被添加PFLAG_FOCUSED标志之后,会调用ViewGroup.requestChildFocus方法:

mParent.requestChildFocus(this, this);

根据7.7.4,ViewGroup.requestChildFocus中会让当前ViewGroup的mFocused指向该View,并且递归调用ViewGroup.requestChildFocus:

        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }

递归调用的debug情况是:

1)、MyLayout.requestChildFocus

在这里插入图片描述

MyLayout的mFocused指向EditText。

2)、FrameLayout.requestChildFocus

在这里插入图片描述

FrameLayout的mFocused指向MyLayout。

3)、LinearLayout.requestChildFocus

在这里插入图片描述

LinearLayout的mFocused指向FrameLayout。

4)、DecorView.requestChildFocus

在这里插入图片描述

DecorView的mFocused指向LinearLayout。

最终View层级结构中会形成一个自根View,DecorView,到调用View.requestFocus的那个View,EditText,的一条子树:

在这里插入图片描述

该子树上所有的View都是直接或者间接包含焦点的View,KeyEvent事件按照这个子树从上往下进行分发即可。

7.7 View.dispatchKeyEvent

接7.6继续分析最后的View.dispatchKeyEvent。

    /**
     * Dispatch a key event to the next view on the focus path. This path runs
     * from the top of the view tree down to the currently focused view. If this
     * view has focus, it will dispatch to itself. Otherwise it will dispatch
     * the next node down the focus path. This method also fires any key
     * listeners.
     *
     * @param event The key event to be dispatched.
     * @return True if the event was handled, false otherwise.
     */
    public boolean dispatchKeyEvent(KeyEvent event) {
		......

        // 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;
        }

		......
        return false;
    }

7.8.1 OnKeyListener.onKey

        // 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;
        }

如果ListenerInfo类型的成员变量mListenerInfo中的OnKeyListener类型的成员变量mOnKeyListener不为NULL,说明当前View通过View.setOnKeyListener:

    /**
     * Register a callback to be invoked when a hardware key is pressed in this view.
     * Key presses in software input methods will generally not trigger the methods of
     * this listener.
     * @param l the key listener to attach to this view
     */
    public void setOnKeyListener(OnKeyListener l) {
        getListenerInfo().mOnKeyListener = l;
    }

注册了一个OnKeyListener:

    /**
     * Interface definition for a callback to be invoked when a hardware key event is
     * dispatched to this view. The callback will be invoked before the key event is
     * given to the view. This is only useful for hardware keyboards; a software input
     * method has no obligation to trigger this listener.
     */
    public interface OnKeyListener {
        /**
         * Called when a hardware key is dispatched to a view. This allows listeners to
         * get a chance to respond before the target view.
         * <p>Key presses in software keyboards will generally NOT trigger this method,
         * although some may elect to do so in some situations. Do not assume a
         * software input method has to be key-based; even if it is, it may use key presses
         * in a different way than you expect, so there is no way to reliably catch soft
         * input key presses.
         *
         * @param v The view the key has been dispatched to.
         * @param keyCode The code for the physical key that was pressed
         * @param event The KeyEvent object containing full information about
         *        the event.
         * @return True if the listener has consumed the event, false otherwise.
         */
        boolean onKey(View v, int keyCode, KeyEvent event);
    }

这允许KeyEvent在分发给焦点View之前,给OnKeyListener一个处理KeyEvent的机会。这里处理的都是硬件键盘产生的KeyEvent,软键盘生成的KeyEvent通常不会触发这个方法。

7.8.2 由当前View来处理KeyEvent

        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }

如果OnKeyListener不处理本次事件,那么由当前View来处理本次事件。

这里调用了KeyEvent.dispatch方法:

    /**
     * Deliver this key event to a {@link Callback} interface.  If this is
     * an ACTION_MULTIPLE event and it is not handled, then an attempt will
     * be made to deliver a single normal event.
     *
     * @param receiver The Callback that will be given the event.
     * @param state State information retained across events.
     * @param target The target of the dispatch, for use in tracking.
     *
     * @return The return value from the Callback method that was called.
     */
    public final boolean dispatch(Callback receiver, DispatcherState state,
            Object target) {
        switch (mAction) {
            case ACTION_DOWN: {
				......
                boolean res = receiver.onKeyDown(mKeyCode, this);
                if (state != null) {
                    if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
						......
                    } else if (isLongPress() && state.isTracking(this)) {
                        try {
                            if (receiver.onKeyLongPress(mKeyCode, this)) {
							......
                            }
                        } catch (AbstractMethodError e) {
                        }
                    }
                }
                return res;
            }
            case ACTION_UP:
				......
                return receiver.onKeyUp(mKeyCode, this);
            ......    
        }
        return false;
    }

这里传入的Callback类型的receiver参数是VIew自身,View实现了KeyEvent.Callback接口,那么最终会调用VIew的onKeyDown、onKeyLongPress、onKeyUp和onKeyMultiple来处理当前事件。

7.8 小结

1)、KeyEvent的处理顺序优先级是,View Hierarchy > Activity > PhoneWindow,首先由View的onKeyDown和onKeyUp等方法去处理KeyEvent。如果View Hierarchy中找不到View可以处理KeyEvent,那么再调用Activity的onKeyDown和onKeyUp等方法去处理KeyEvent。如果Activity也处理不了,那么最后由PhoneWindow的onKeyDown和onKeyUp等方法去处理KeyEvent。

2)、在分发KeyEvent之前,View Hierarchy中需要先构建一个自根View至下的一个焦点VIew子树,KeyEvent只会在这个子树中进行分发,不会像MotionEvent一样遍历当前ViewGroup的所有子VIew去寻找满足接收条件的子View。这个思路和InputDispatcher向窗口分发事件是一样的,对于key类型的事件,由于这类事件不像Touch事件一样有坐标,因此我们只能事先设置一个焦点窗口/View,然后由这个焦点窗口/View来接受这个事件,否则那么多窗口/View,我们根本不知道应该把事件发送给谁。对于Touch类型的事件,由于这类事件是有坐标的,因此我们可以根据坐标还有其他一些规则,通过对窗口/View进行遍历来找到可以接收当前事件的窗口/VIew,这就不再依赖对焦点窗口/View的设置。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值