从源码分析key事件的传递机制

开发中需要用到遥控器,各种上下左右菜单音量飞鼠OK按键满天飞...
对于key事件的捕获仅限于BACK/MENU/HOME按键的我来说,这完全是在搞事情啊!
因此,决定深刻认识一下,从源码角度(android-23)来理解key事件的传递机制。
首先从入口开始,直接看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
        if (event.getKeyCode() == 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);
    }

分析源码可知,事件传递的优先顺序如下:
actionBar——>Window——>event.dispatch,下面按照这个顺序依次分析

一、actionBar

keyCode如果是KEYCODE_MENU,且actionBar存在,
则交给了actionBar处理:使用其onMenuKeyEvent方法,对菜单按键进行响应;
如果actionBar不消费,就交给Window处理

二、Window

Window对key事件的处理与Touch事件有些类似,都是View树中的传递,
不过Touch事件是由父到子层层往下传递,而key事件的传递是由焦点位置决定的。
下面按层级从Window——>View逐个分析

1、Window.superDispatchKeyEvent
如果actionBar不消费,事件传递给Window,
window直接调用了上述代码中的Window.superDispatchKeyEvent方法,
Window.superDispatchKeyEvent源码如下:

    /**
     * Used by custom windows, such as Dialog, to pass the key press event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchKeyEvent(KeyEvent event);

注释已经说得很清楚:
*“该方法用于自定义窗口下(如dialog、Activity),key事件在View层级中的传递,开发过程中不需要自己去实现或调用这个方法。” *
Window是一个抽象类,Window#superDispatchKeyEvent也是一个抽象方法,
这个方法具体都做了些什么,我们到Window的唯一实现类PhoneWindow去查看,

2、PhoneWindow.superDispatchKeyEvent
PhoneWindow.superDispatchKeyEvent源码如下:

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

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

我们看到,PhoneWindow中直接调用了DecorView的superDispatchKeyEvent方法
简单介绍一下DecorView:
它是Activity的顶层容器,整个View树结构的最顶层View,代表整个应用的界面。Window、DecorView、Activity三者关系可以这样类比:
Window是窗户,DecorView是粘在窗户上的纸,Activity显示的内容就是纸上所画的内容。
DecorView分为title和content两部分,而这个content,也就是我们在Activity中setContentView所设置的布局View。

DecorView.superDispatchKeyEvent:

      public boolean superDispatchKeyEvent(KeyEvent event) {
            // Give priority to closing action modes if applicable.
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                final int action = event.getAction();
                // Back cancels action modes first.
                if (mPrimaryActionMode != null) {
                    if (action == KeyEvent.ACTION_UP) {
                        mPrimaryActionMode.finish();
                    }
                    return true;
                }
            }

            return super.dispatchKeyEvent(event);
        }
  • DecorView在keyCode == KECODE_BACK时,检查是否有Menu弹出,有则毁MENU,防止内存泄漏
    (mPrimaryActionMode是一种菜单方式,这里使用它销毁Menu)
  • 不是BACK按键时,return super.dispatchKeyEvent,即交给父类处理,
    因为DecorView继承于FrameLayout,所以super.dispatchKeyEvent其实就是ViewGroup.dispatchKeyEvent,key事件在View层级中的传递最终在这里进行,所以我们再看ViewGroup的dispatchKeyEvent方法。

3、ViewGroup.dispatchKeyEvent
ViewGroup.dispatchKeyEvent源码:

    // The view contained within this ViewGroup that has or contains focus.
    private View mFocused;

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

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
        }
        return false;
    }
  • mInputEventConsistencyVerifier主要在debug的时候使用,暂不深究
  • super.dispatchKeyEvent:
    如果ViewGroup自身已经获得焦点,就调用super.dispatchKeyEvent,即View.dispatchKeyEvent
  • mFocus.dispatchKeyEvent:
    如果ViewGroup没有获得焦点,而是ViewGroup中的某一个child获得了焦点,则把事件交给这个child,即上述代码中的mFocused,将key事件传递给焦点子View处理,也是View.dispatchKeyEvent
  • 如果都没有获取焦点,直接return false,不消费,所以ViewGroup.dispatchKeyEvent中,只有当ViewGroup或其child获取了焦点,才有机会去消费这个事件,而具体怎么消费,都交给了View的dispatchKeyEvent方法
    继续往下,查看View.dispatchKeyEvent

4、View.dispatchKeyEvent
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) {
        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;
    }
  • dispatchKeyEvent做了什么?先看看注释怎么说:“将key事件派发到焦点路径上的某一个View,这个焦点路径遵循View树结构自顶向下的规则,如果当前View获得焦点,事件就派发给它自己,否则继续在焦点路径上往下派发。该方法也会触发KeyListener。
  • 如果View设置了KeyListener,且该View是启用状态,则将事件交给它设置的KeyListener处理
  • 如果KeyListener不消费,或者没有KeyListener,则调用KeyEvent的dispatch方法
    KeyEvent.dispath的第一个参数this,其实就是在View中实现的接口:KeyEvent.CallBack
    (KeyEvent.CallBack在View和Activity中都有被实现)

5、KeyEvent.dispatch
(1)KeyEvent.dispatch源码:

   public final boolean dispatch(Callback receiver, DispatcherState state,
            Object target) {
        switch (mAction) {
            case ACTION_DOWN: {
                mFlags &= ~FLAG_START_TRACKING;
                if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
                        + ": " + this);
                boolean res = receiver.onKeyDown(mKeyCode, this);
                if (state != null) {
                    if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
                        if (DEBUG) Log.v(TAG, "  Start tracking!");
                        state.startTracking(this, target);
                    } else if (isLongPress() && state.isTracking(this)) {
                        try {
                            if (receiver.onKeyLongPress(mKeyCode, this)) {
                                if (DEBUG) Log.v(TAG, "  Clear from long press!");
                                state.performedLongPress(this);
                                res = true;
                            }
                        } catch (AbstractMethodError e) {
                        }
                    }
                }
                return res;
            }
            case ACTION_UP:
                if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
                        + ": " + this);
                if (state != null) {
                    state.handleUpEvent(this);
                }
                return receiver.onKeyUp(mKeyCode, this);
            case ACTION_MULTIPLE:
                final int count = mRepeatCount;
                final int code = mKeyCode;
                if (receiver.onKeyMultiple(code, count, this)) {
                    return true;
                }
                if (code != KeyEvent.KEYCODE_UNKNOWN) {
                    mAction = ACTION_DOWN;
                    mRepeatCount = 0;
                    boolean handled = receiver.onKeyDown(code, this);
                    if (handled) {
                        mAction = ACTION_UP;
                        receiver.onKeyUp(code, this);
                    }
                    mAction = ACTION_MULTIPLE;
                    mRepeatCount = count;
                    return handled;
                }
                return false;
        }
        return false;
    }
  • dispatch方法中,根据KeyEvent.action进行判断:
    ACTION_DOWN调用receiver.onKeyDown
    ACTION_UP调用receiver.onKeyUp
    ACTION_MULTIPLE调用receiver.onKeyMultiple

  • 那么这个receiver来自哪里?
    回溯到View.dispatchKeyEvent中,调用event.dispatch时传入了this,查看View实现的接口就豁然开朗了

@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {

显然,这里的receiver就是View本身,所以KeyEvent的dispatch方法将事件分了一下类别,然后又交给了View中对应的方法去处理,接下来看看KeyEvent.Callback在View中的实现

6、KeyEvent.Callback
KeyEvent中对Callback的定义如下:

   public interface Callback {

        boolean onKeyDown(int keyCode, KeyEvent event);

        boolean onKeyLongPress(int keyCode, KeyEvent event);

        boolean onKeyUp(int keyCode, KeyEvent event);

        boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
    }
  • KeyEvent的Callback接口主要包含onKeyDown、onKeyUp、onKeyLongPress、onKeyMultiPe方法,这不就是平时开发中,接收key事件常常复写的那个onKeyDown吗

View中对Callback的实现如下:

    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
        return false;
    }

    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
        return false;
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        boolean result = false;

        if (KeyEvent.isConfirmKey(keyCode)) {
            if ((mViewFlags & ENABLED_MASK) == DISABLED) {
                return true;
            }
            // Long clickable items don't necessarily have to be clickable
            if (((mViewFlags & CLICKABLE) == CLICKABLE ||
                    (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
                    (event.getRepeatCount() == 0)) {
                setPressed(true);
                checkForLongClick(0);
                return true;
            }
        }
        return result;
    }

    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (KeyEvent.isConfirmKey(keyCode)) {
            if ((mViewFlags & ENABLED_MASK) == DISABLED) {
                return true;
            }
            if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
                setPressed(false);

                if (!mHasPerformedLongPress) {
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();
                    return performClick();
                }
            }
        }
        return false;
    }

     /** Whether key will, by default, trigger a click on the focused view.
     * @hide
     */
    public static final boolean isConfirmKey(int keyCode) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_CENTER:
            case KeyEvent.KEYCODE_ENTER:
                return true;
            default:
                return false;
        }
    }
  • View的onKeyLongPress和onKeyMultiPe方法都默认返回了false,
  • View的onKeyDown和onKeyUp方法中默认使用isConfirmKey进行判断,
    查看上述源码可知,isConfirmKey只在ENTER或PPAD_CENTER时返回true,即遥控器上的确认键或OK键,
    所以onKeyDown和onKeyUp在默认情况下只消费ENTER和CENTER这种确认类型的按键
  • 如果View的onKeyDown、onKeyUp等KeyEvent.Callback的实现方法都不消费,表示Activity.dispatchKeyEvent将事件派发给Window没有被消费,事件继续往下传递,到了Activity.dispatchKeyEvent方法中的第三部分——交给Activity中的event.dispatch处理

7、最后,总结一下Key事件在Window传递的整个流程:
1、 如果是BACK按键,先交给MENU处理,
2、如果不是BACK按键或MENU未处理,交给ViewGroup传递给它的焦点View:焦点View可以是ViewGroup本身或ViewGroup的child,如果焦点View设置了KeyListener,传递给KeyListener,如果没有,按KeyEvent.action类型传递给对应的onKeyDown等方法
3、 如果事件传递到了onKeyDown都没被处理,代表Window传递到它的View各个层级都没有消费该事件,回溯到Activity.dispatchKeyEvent继续往下传递

  • 因为自己曾经在这里踩过坑,再次强调,重要的事情说三遍,只有ViewGroup或childView获取了焦点,ViewGroup才有机会消费key事件,换句话说,当ViewGroup或childView没有焦点时,key事件会往下传递,即最终交给了Activity.dispatchKeyEvent的第三步骤——event.dispatch

三、event.dispatch交给自己的onKeyDown

1、如果window不消费,最后直接执行了keyEvent.dispatch方法,这里的逻辑与View中的keyEvent.dispatch完全相同,只是Callback在Activity中实现,所以key事件都到了Activity的onKeyDown、onKeyUp....
2、注意:
正因为key事件从dispatchKeyEvent分发到onKeyDown、onKeyUp等是由envet.dispatch实现的,
所以如果用户在Activity中复写了dispatchKeyEvent方法,
如果返回true或false,key事件都不会到Activity的onKeyDown或onKeyUp中,
只有返回super.dispatchKeyEvent,执行到了event.dispatch,key事件才能够派发到Activity的onKeyDown或onKeyUp中,这是不同于dispatchTouchEvent的地方

四、Activity中Key事件传递流程总结

activity中先在dispatch函数中接收到事件,
1、key事件首先交给了actionBar处理
2、如果actionBar未处理,则交给window处理,window直接交给了DecorView.superDispatchKeyEvent,将事件传递给了获取焦点的View
4、如果焦点view未处理,继续往后传递,到了KeyEvent.dispatch,最终KeyEvent.dispatch将事件交给Activity的onKeyDown、onKeyUp、onKeyMultiPe或onKeyLongPress处理。
5、如果Activity的onKeyDown、onKeyUp、onKeyMultiPe或onKeyLongPress都没处理,即Activity本身及其ContentView都没有消费该事件,最终该事件就交给PhoneWindow或ViewRootImpl自动处理:自动查找焦点、系统音量调节等

整个事件分发流程图如下:

Key传递流程.png

五、Activity中Key事件与Touch事件传递机制的区别

  • Touch事件:
    Touch事件从Activity到Window,进行View树结构的分发,首先是顶层DecorView,再到ViewGroup,然后传递到子View按层级逐一向下分发,如果View树结构的分发都不消费,就回传给Activity的onTouchEvent处理

  • Key事件:
    Key事件从Activity先判断ActionBar需不需要,再判断Menu需不需要,然后交给Window在View树结构中分发,
    同样首先也是顶层DecorView,DecorView直接在View层级间根据焦点位置决定谁获得key事件,若View各层级都不需要,再回到Activity自身的onKeyDown中

以上分析,如果有错误或任何建议的地方,欢迎指正,一起交流学习!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容