Android输入子系统之java层按键传递

平台:Android6.0

Android开发中在自定义Activity以及View时经常会重写onKeyDown,onKeyUp,dispatchKeyEvent,同时View还有setOnKeyListener等,当一个按键事件发生时,这些方法将会被回调,但是到底哪个先回调,哪个后回调呢,一直不是特别清楚,只知道个大概,下面将详细讲述按键在java层的分发过程,其中重点关注按键事件在View层次中的分发

java层的按键分发从ViewRootImpl.java的WindowInputEventReceiver中的onInputEvent开始,从前面的应用程序注册消息监听过程分析和InputDispatcher分发键盘消息过程分析两篇文章的分析,InputDispatcher在处理按键事件时,会通过InputChannel::sendMessage函数将按键消息从server端写入,这里的InputChannel是当前获取焦点的窗口的InputChannel对的server端,这样应用程序端就可以收到该消息,然后调用NativeInputEventReceiver的handleEvent,最后调用到InputEventReceiver的onInputEvent函数(具体的可以看应用程序注册消息监听过程分析 的Step20-Step23)

Step 1. WindowInputEventReceiver.onInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java

final class WindowInputEventReceiver extends InputEventReceiver {    public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {        super(inputChannel, looper);    }    @Override    public void onInputEvent(InputEvent event) {        enqueueInputEvent(event, this, 0, true);    }    ...}

这里只列出部分代码,当一个按键按下时onInputEvent方法就会被回调,其中调用了ViewRootImpl::enqueueInputEvent(event, this, 0, true);

ViewRootImpl.enqueueInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java

void enqueueInputEvent(InputEvent event,        InputEventReceiver receiver, int flags, boolean processImmediately) {    //从队列中获取一个QueuedInputEvent,这里的flags传入的是0    QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);    ...    if (processImmediately) {        doProcessInputEvents();//这里传入的processImmediately是true,所以调用doProcessInputEvents    } else {        scheduleProcessInputEvents();    }}

从前面的参数可知,这里表示要立即处理,所以调用doProcessInputEvents函数.
ViewRootImpl.doProcessInputEvents
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java

void doProcessInputEvents() {    // Deliver all pending input events in the queue.    while (mPendingInputEventHead != null) {        QueuedInputEvent q = mPendingInputEventHead;        mPendingInputEventHead = q.mNext;        if (mPendingInputEventHead == null) {            mPendingInputEventTail = null;        }        q.mNext = null;        mPendingInputEventCount -= 1;        ...        //分发按键        deliverInputEvent(q);    }}

在deliverInputEvent函数中实际做按键的分发
ViewRootImpl.deliverInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java

private void deliverInputEvent(QueuedInputEvent q) {    Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",            q.mEvent.getSequenceNumber());    if (mInputEventConsistencyVerifier != null) {        mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);    }    InputStage stage;    if (q.shouldSendToSynthesizer()) {        stage = mSyntheticInputStage;    } else {        //选择责任链的模式的入口,如果InputEvent需要跳过IME处理,则从mFirstPostImeInputStage(EarlyPostImeInputStage)开始,否则从mFirstInputStage(NativePreImeInputStage)开始分发        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;    }    if (stage != null) {        stage.deliver(q);    } else {        finishInputEvent(q);    }}

这里调用了InputStage的deliver方法分发,这里的InputStage代表了输入事件的处理阶段,是一种责任链模式(可以看我的另外一篇文章责任链模式)

在ViewRootImpl的setView函数中会构造一个如图所示的InputStage的链,按键会从入口阶段,进入责任链,顺序处理,入口阶段根据QueuedInputEvent的状态来决定。q.shouldSendToSynthesizer() 这里一般是false,因此主要看stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; 这里的shouldSkipIme其实是一个flag在构造QueuedInputEvent时传入的,从前面的onInputEvent调用的enqueueInputEvent(event, this, 0, true);可知,这里传入的flags是第三个参数0,那这里的shouldSkipIme就是false,那么按键会从mFirstPostImeInputStage 开始分发,就是图中的NativePreImeInputStage分发。

下面只从跟本文前面提到的Activity,View的按键分发流程相关的InputStage(ViewPostImeInputStage)开始分析

ViewPostImeInputStage.processKeyEvent

该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java

private int processKeyEvent(QueuedInputEvent q) {    final KeyEvent event = (KeyEvent)q.mEvent;    ...    // Deliver the key to the view hierarchy.    // 调用成员变量mView的dispatchKeyEvent函数,这里mView是PhoneWindow.DecorView对象    if (mView.dispatchKeyEvent(event)) {        return FINISH_HANDLED;    }    ...    // 如果按键是四向键或者是TAB键,则移动焦点    // Handle automatic focus changes.    if (event.getAction() == KeyEvent.ACTION_DOWN) {        int direction = 0;        switch (event.getKeyCode()) {            case KeyEvent.KEYCODE_DPAD_LEFT:                if (event.hasNoModifiers()) {                    direction = View.FOCUS_LEFT;                }                break;            case KeyEvent.KEYCODE_DPAD_RIGHT:                if (event.hasNoModifiers()) {                    direction = View.FOCUS_RIGHT;                }                break;            case KeyEvent.KEYCODE_DPAD_UP:                if (event.hasNoModifiers()) {                    direction = View.FOCUS_UP;                }                break;            case KeyEvent.KEYCODE_DPAD_DOWN:                if (event.hasNoModifiers()) {                    direction = View.FOCUS_DOWN;                }                break;            case KeyEvent.KEYCODE_TAB:                if (event.hasNoModifiers()) {                    direction = View.FOCUS_FORWARD;                } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {                    direction = View.FOCUS_BACKWARD;                }                break;        }        if (direction != 0) {            View focused = mView.findFocus();            if (focused != null) {                View v = focused.focusSearch(direction);                if (v != null && v != focused) {                    // do the math the get the interesting rect                    // of previous focused into the coord system of                    // newly focused view                    focused.getFocusedRect(mTempRect);                    if (mView instanceof ViewGroup) {                        ((ViewGroup) mView).offsetDescendantRectToMyCoords(                                focused, mTempRect);                        ((ViewGroup) mView).offsetRectIntoDescendantCoords(                                v, mTempRect);                    }                    if (v.requestFocus(direction, mTempRect)) {                        playSoundEffect(SoundEffectConstants                                .getContantForFocusDirection(direction));                        return FINISH_HANDLED;                    }                }                // Give the focused view a last chance to handle the dpad key.                if (mView.dispatchUnhandledMove(focused, direction)) {                    return FINISH_HANDLED;                }            } else {                // find the best view to give focus to in this non-touch-mode with no-focus                View v = focusSearch(null, direction);                if (v != null && v.requestFocus(direction)) {                    return FINISH_HANDLED;                }            }        }    }    return FORWARD;}

上述主要分两步:
第一步是调用PhoneWindow.DecorView的dispatchKeyEvent函数,DecorView是View层次结构的根节点,按键从根节点开始根据Focuse view的path自上而下的分发。
第二步是判断按键是否是四向键,或者是TAB键,如果是则需要移动焦点

public boolean dispatchKeyEvent(KeyEvent event) {      ...    if (!isDestroyed()) {        final Callback cb = getCallback();        final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)                : super.dispatchKeyEvent(event);        if (handled) {            return true;        }        }        return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event)            : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);}

主要的分发在下面开始,如果cb不为空并且mFeatureId小于0,则调用cb.dispatchKeyEvent开始分发,否则会调用DecorView的父类(View)的dispatchKeyEvent函数。cb是Window.Callback类型,Activity实现了Window.Callback接口,在attach函数中,会调用Window.setCallback函数将自己注册进PhoneWindow中,所以cb不为空。在PhoneWindow初始化时会调用installDecor函数生成DecorView对象,该函数中传入的mFeatureId是-1,所以mFeatureId也小于0。因此此处会调用Activity的dispatchKeyEvent函数,开始在View中分发按键。

下面来分析按键在View的层次结构中是如何分发的

接下来来看这里先看看Activity(Callback)的dispatchKeyEvent实现:

Activity.dispatchKeyEvent

该函数定义在frameworks/base/core/java/android/app/Activity.java

public boolean dispatchKeyEvent(KeyEvent event) {    //调用自定义的onUserInteraction    onUserInteraction();    Window win = getWindow();    //调用PhoneWindow的superDispatchKeyEvent,实际调用DecorView的superDispatchKeyEvent,从DecorView开始从顶层View往子视图传递    if (win.superDispatchKeyEvent(event)) {          return true;    }    View decor = mDecor;    if (decor == null) decor = win.getDecorView();    //到这里如果view层次结构没有返回true则交给KeyEvent本身的dispatch方法,Activity的onKeyDown/onKeyUp/onKeyMultiple就会被触发    return event.dispatch(this, decor != null            ? decor.getKeyDispatcherState() : null, this);}

接着看下PhoneWindow的superDispatchKeyEvent

PhoneWindow.superDispatchKeyEvent

@Overridepublic boolean superDispatchKeyEvent(KeyEvent event) {    return mDecor.superDispatchKeyEvent(event);} 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;         }    }    //进入View的层次结构,调用ViewGroup.dispatchKeyEvent    return super.dispatchKeyEvent(event);}

再看ViewGroup的dispatchKeyEvent函数

ViewGroup.dispatchKeyEvent

该函数定义在frameworks/base/core/java/android/view/ViewGroup.java

public boolean dispatchKeyEvent(KeyEvent event) {    ...    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {        //如果此ViewGroup是focused并且具体的大小被设置了(有边界),则交给它处理,即调用View的实现        if (super.dispatchKeyEvent(event)) {            return true;        }        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)            == PFLAG_HAS_BOUNDS) {        //否则,如果此ViewGroup中有focused的child,且child有具体的大小,则交给mFocused处理        if (mFocused.dispatchKeyEvent(event)) {            return true;        }        }        ...    return false;}

这里可以看出如果ViewGroup满足条件,则优先处理事件而不发给子视图去处理。

下面看下View的dispatchKeyEvent实现

View.dispatchKeyEvent

该函数定义在frameworks/base/core/java/android/view/View.java

public boolean dispatchKeyEvent(KeyEvent event) {    ...    // Give any attached key listener a first crack at the event.    //noinspection SimplifiableIfStatement    ListenerInfo li = mListenerInfo;    //调用onKeyListener,如果注册了OnKeyListener,并且View属于Enable状态,则触发    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {        return true;     }         //调用KeyEvent.dispatch方法,并将view作为参数传递进去,实际会回调View的onKeyUp/onKeyDown等方法    if (event.dispatch(this, mAttachInfo != null            ? mAttachInfo.mKeyDispatchState : null, this)) {        return true;     }         ...      return false;}

View.onKeyDown/View.onKeyUp

该函数定义在frameworks/base/core/java/android/view/View.java
下面看下View的onKeyDown/onKeyUp

// frameworks/base/core/java/android/view/View.javapublic boolean onKeyDown(int keyCode, KeyEvent event) {    boolean result = false;    //处理KEYCODE_DPAD_CENTER、KEYCODE_ENTER按键    if (KeyEvent.isConfirmKey(keyCode)) {        if ((mViewFlags & ENABLED_MASK) == DISABLED) {            //disabled的view直接返回true,不再继续分发,即Activity的onKeyDown和onKeyUp无法收到KEYCODE_DPAD_CENTER、KEYCODE_ENTER事件            return true;        }        // Long clickable items don't necessarily have to be clickable        if (((mViewFlags & CLICKABLE) == CLICKABLE ||                (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&                (event.getRepeatCount() == 0)) {// clickable或者long_clickable且是第一次down事件            setPressed(true);// 标记pressed,你可能设置了View不同的background,这时候就会有所体现(比如高亮效果)            checkForLongClick(0);            return true;        }    }    return result;}public boolean onKeyUp(int keyCode, KeyEvent event) {    //处理KEYCODE_DPAD_CENTER、KEYCODE_ENTER按键    if (KeyEvent.isConfirmKey(keyCode)) {        if ((mViewFlags & ENABLED_MASK) == DISABLED) {            //disabled的view直接返回true,不再继续分发,即Activity的onKeyDown和onKeyUp无法收到KEYCODE_DPAD_CENTER、KEYCODE_ENTER事件            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;}

Activity.onKeyDown/onKeyUp

最后再分析一下Activity的onKeyDown/onKeyUp

// frameworks/base/core/java/android/app/Activity.javapublic boolean onKeyDown(int keyCode, KeyEvent event)  {    //如果是back键则启动追踪    if (keyCode == KeyEvent.KEYCODE_BACK) {        if (getApplicationInfo().targetSdkVersion                >= Build.VERSION_CODES.ECLAIR) {            event.startTracking();        } else {            onBackPressed();        }            return true;    }        ...}public boolean onKeyUp(int keyCode, KeyEvent event) {    if (getApplicationInfo().targetSdkVersion            >= Build.VERSION_CODES.ECLAIR) {        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()                && !event.isCanceled()) {            //如果是back键并且正在追踪该Event,则调用onBackPressed            onBackPressed();            return true;        }    }    return false;}

总结
1. 调用Activity的dispatchKeyEvent
1.1. 调用onUserInteraction

1.2. 调用PhoneWindow的superDispatchKeyEvent
1.2.1.再然后调用DecorView的superDispatchKeyEvent,DecorView的superDispatchKeyEvent再调用父类的dispatchKeyEvent
1.2.2. DecorView是窗口中的顶级视图,按键从DecorView开始往子节点分发,因为DecorView继承自FrameLayout,FrameworkLayout继承自ViewGroup,所以实际调用ViewGroup的dispatchKeyEvent

1.2.3. ViewGroup中会先判断是否可以处理KeyEvent,如果可以则调用父类(View)的dispatchKeyEvent,如果当前的ViewGroup不满足条件,则调用mFocused的dispatchKeyEvent,这里的mFocused是焦点子视图,也可以是含有焦点子视图的ViewGroup,因此这里可能会发生递归调用。
1.2.3.1. 在View的dispatchKeyEvent中会先调用onKey函数,即会调用各个View注册的View.OnKeyListener对象的接口
1.2.3.2. 在View的dispatchKeyEvent中接着调用KeyEvent的dispatch函数,因为View实现了Window.Callback函数,因此会调用View的onKeyDown/onKeyUp/onKeyMultiple函数

1.3 调用KeyEvent的dispatch函数,因为Activity实现了Window.Callback函数,因此会调用Activity的onKeyDown/onKeyUp/onKeyMultiple函数
2. 如果整个View层次都没有返回true,则调用PhoneWindow的onKeyDown/onKeyUp函数

总结:
以一次KEYCODE_DPAD_DOWN按键为例,说明各个回调的顺序
Down事件
Activity::dispatchKeyEvent –> Activity::onUserInteraction –> View.OnKeyListener::onKey –> View.onKeyDown –> Activity::onKeyDown –> PhoneWindow.onKeyDown
Up事件
Activity::dispatchKeyEvent –> Activity::onUserInteraction –> View.OnKeyListener::onKey –> View.onKeyUp –> Activity::onKeyUp –> PhoneWindow.onKeyUp

View的KEYCODE_DPAD_CENTER KEYCODE_ENTER是在View的onKeyDown和onKeyUp中处理的,会处理一些高亮效果

一个Enable并且CLICKABLE的View响应KEYCODE_DPAD_CENTER KEYCODE_ENTER的DOWN事件后会return true,即Activity的onKeyDown不会回调,但是up事件没有return true,Activity的onKeyUp会回调

Activity的KEYCODE_BACK在Activity的onKeyDown启动追踪,在onKeyUp中实际调用onBackPressed函数处理返回键,所以长按back键是不会切换Activity的

参考:
http://www.cnblogs.com/xiaoweiz/p/3803301.html

更多相关文章

  1. C语言函数的递归(上)
  2. Android:Android系统启动(笔记)
  3. Android快速开发 动画系列(二) 之 overridePendingTransition(跳转
  4. android号码匹配位的修改
  5. 通过广播获取Android屏幕旋转事件
  6. Android中getWidth()和getMeasureWidth()的区别探究
  7. android 7.0 加入 android:directBootAware
  8. Android工程师面试准备知识点
  9. Android(安卓)ndk开发swig编译jni接口配置文件(二)

随机推荐

  1. Android inject input events 注入Touch
  2. Flex开发Android(安卓)更改ViewMenu外观
  3. Deepin Linux系统中开启ap-hotspot wifi
  4. 【Android开发小记--9】触摸事件---实现
  5. Android自定义view三验证码输入控件
  6. android 输入法界面显示的开关
  7. Android和IOS开发资料
  8. Android setLayerType 硬件加速问题
  9. android studio中.9.png图片处理
  10. 给动态生成的View添加水波纹效果