Android事件全流程介绍

转载请注明出处:http://www.jianshu.com/p/76b3e1913f51

想写点什么东西放在这里,到后来发现没什么用,直接进入正题吧,本文主要是由于工作的遇到的一个问题,之后自己闲的蛋疼研究研究之后一发不可收拾,整篇文章从 Framework层开始一直到我们平时处理事件的View进行了整体的介绍,说是分析,其实就是梳理一遍事件的传递的流程,不要说我是标题党就好。由于时间及经验有限,文中可能存在错误与不足,欢迎大家指出,我会第一时间对文章进行修改纠正。

在说正文之前,必须要感谢几个人:以我遇到的时间排序。

  1. Android按键事件处理流程 -KeyEvent,作者tmp_zhao目前已经迁到简书。这篇文章介绍了事件,准确的说是按键事件(KeyEvent)从DecorView到View的整体流程。
  2. Android 中keyEvent的消息处理,作者是转载的,没有找到原作者。这篇文章介绍事件是怎么从连接层传到DecorView的,简单介绍,没有过多说明。
  3. Android源码解析(三十)-->触摸事件分发流程,作者一片枫叶_刘超,为我之前的猜想提供了充分有力的证据。
  4. Android中事件传递分析,作者zjutkz提供了从 Framework层到ViewRootImpl的证据,大神。

非常感谢以上四位大神的分享。


整体流程:系统核心->Native层->ViewRootImpl->DecorView->Activity->Window->DecorView->ViewGroup->View,无论是触摸事件还是点击时间都是一样的。
系统核心传到Framework的不去讨论了,我也不会,这篇文章是从ViewRootImpl开始说,关于怎么从Framework传到ViewRootImpl的,有兴趣的可以去看上面推荐的文章。

ViewRootImpl

首先从Framework中传递过来的事件会到InputEventReceiver的onInputEvent方法,在ViewRootImpl中的它的实现类是:

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

        @Override
        public void onBatchedInputEventPending() {
            if (mUnbufferedInputDispatch) {
                super.onBatchedInputEventPending();
            } else {
                scheduleConsumeBatchedInput();
            }
        }

        @Override
        public void dispose() {
            unscheduleConsumeBatchedInput();
            super.dispose();
        }
    }

当一个事件产生,通过一系列的传递,最终会到WindowInputEventReceiver的onInputEvent方法中进行排队处理,之后会调到ViewRootImpl的enqueueInputEvent方法:

    void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
        adjustInputEventForCompatibility(event);
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

        // Always enqueue the input event in order, regardless of its time stamp.
        // We do this because the application or the IME may inject key events
        // in response to touch events and we want to ensure that the injected keys
        // are processed in the order they were received and we cannot trust that
        // the time stamp of injected events are monotonic.
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            last.mNext = q;
            mPendingInputEventTail = q;
        }
        mPendingInputEventCount += 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);
        <------Look here------>
        if (processImmediately) {
            doProcessInputEvents();
        } else {
            scheduleProcessInputEvents();
        }
    }

不考虑延迟的情况,直接看doProcessInputEvents():

    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;
            Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                    mPendingInputEventCount);

            long eventTime = q.mEvent.getEventTimeNano();
            long oldestEventTime = eventTime;
            if (q.mEvent instanceof MotionEvent) {
                MotionEvent me = (MotionEvent)q.mEvent;
                if (me.getHistorySize() > 0) {
                    oldestEventTime = me.getHistoricalEventTimeNano(0);
                }
            }
            mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);

            deliverInputEvent(q);
        }

        // We are done processing all input events that we can process right now
        // so we can clear the pending flag immediately.
        if (mProcessInputEventsScheduled) {
            mProcessInputEventsScheduled = false;
            mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
        }
    }

上面的代码中最重要的就是deliverInputEvent(q)这句,其他的都是一些排列啊,获得事件触发的事件啊,之类额,不是很懂,也不过介绍了。继续往下看:

     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 {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }
        <------------重点----------------->
        if (stage != null) {
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

在这个方法中会创建一个InputStage对象,有兴趣的可以去看一下具体这个类是怎么写的,蛮有意思的,这里就不过多介绍了,只说两个方法,一个是onProcess这个是处理event的方法,另一个是forward,让下一个InputStage去处理。我们可以在ViewRootImpl的setView方法中找到上面出现的三种InputStage:

    mSyntheticInputStage = new SyntheticInputStage();
    InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
    InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
            "aq:native-post-ime:" + counterSuffix);
    InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
    InputStage imeStage = new ImeInputStage(earlyPostImeStage,
            "aq:ime:" + counterSuffix);
    InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
    InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
            "aq:native-pre-ime:" + counterSuffix);
    mFirstInputStage = nativePreImeStage;
    mFirstPostImeInputStage = earlyPostImeStage;

重点是在ViewPostImeInputStage的onProcess方法中:

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

可以看到在这里对于InputEvent事件进行了分别的处理,KeyEvent事件调用java processKeyEvent(q),MotionEvent事件调用java processPointerEvent。我们目前只关注KeyEvent,其实两条路目前是一样的。

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

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }

            if (shouldDropInputEvent(q)) {
                return FINISH_NOT_HANDLED;
            }
            //省略一部分代码
    }

这里的mView,其实就是DecorView。事件就从连接层传到了视图层(这么说可能不太准确,个人理解吧)。

我们来总结一下在ViewRootImpl中都做了哪些事情:

  1. 在WindowInputEventReceiver这个接口中接受从Framework层传递过来的输入事件。
  2. 使用QueuedInputEvent对所有的事件进行排队,然后根据不同的条件,状态把事件分发下去。
  3. 在处理事件的时候使用InputStage能够实现对事件的按照顺序处理。
  4. 在ViewPostImeInputStage对事件进行分发,根据事件类别分发到DecorView中。

以上就是在ViewRootImpl中进行的处理。

DecorView

接着上文,我们继续往下分析,这里只针对KeyEvent。先来看看java dispatchKeyEvent(p)这个方法:

使用的是API24的版本的源码,跟之前版本的可能会有所区别,不过大体上是一样的。

<!--DecorView.java-->
 public boolean dispatchKeyEvent(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int action = event.getAction();
        final boolean isDown = action == KeyEvent.ACTION_DOWN;
        /// 1. 第一次down事件的时候,处理panel的快捷键
        if (isDown && (event.getRepeatCount() == 0)) {
            // First handle chording of panel key: if a panel key is held
            // but not released, try to execute a shortcut in it.
            if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
                boolean handled = dispatchKeyShortcutEvent(event);
                if (handled) {
                    return true;
                }
            }

            // If a panel is open, perform a shortcut on it without the
            // chorded panel key
            if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
                if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
                    return true;
                }
            }
        }
        /// 2. 这里是我们本文的重点,当window没destroy且其Callback非空的话,交给其Callback处理
        /// 其中 mFeatureId < 0 表示是Application的DecorView
        if (!mWindow.isDestroyed()) {
            final Window.Callback cb = mWindow.getCallback();// Activity、Dialog都是Callback接口的实现
            final boolean handled = cb != null && mFeatureId < 0 ? 
                    cb.dispatchKeyEvent(event)//Activity,Dialog。
                    : super.dispatchKeyEvent(event);//否则直接派发到ViewGroup#dispatchKeyEvent(View层次结构)
            if (handled) {
                return true;
            }
        }
        /// 3. 这是key事件的最后一步,如果到这一步还没处理掉,则派发到PhoneWindow对应的onKeyDown, onKeyUp方法
        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
    }

代码上都有相应的注释,简单解释一下就是如果当前的DecorView是Application的DecorView的话,并且存在Activity或者Dialog,那么会执行Activity或Dialog的java dispatchKeyEvent()方法,否则的话就直接执行ViewGroup的java dispatchKeyEvent()方法。并且如果,没有消耗掉这个事件的话就回去去执行PhoneWindow的onKeyDown或onKeyUp方法。
接下来按照正常的逻辑会调用Activity的java dispatchKeyEvent()方法:

<!-- Activity -- >

 public boolean dispatchKeyEvent(KeyEvent event) {
        onUserInteraction();

        // Let action bars open menus in response to the menu key prioritized over
        // the window handling it
        //处理ActionBar和Menu 略过
        final int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_MENU &&
                mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
            return true;
        } else if (event.isCtrlPressed() &&
                event.getUnicodeChar(event.getMetaState() & ~KeyEvent.META_CTRL_MASK) == '<') {
            // Capture the Control-< and send focus to the ActionBar
            final int action = event.getAction();
            if (action == KeyEvent.ACTION_DOWN) {
                final ActionBar actionBar = getActionBar();
                if (actionBar != null && actionBar.isShowing() && actionBar.requestFocus()) {
                    mEatKeyUpEvent = true;
                    return true;
                }
            } else if (action == KeyEvent.ACTION_UP && mEatKeyUpEvent) {
                mEatKeyUpEvent = false;
                return true;
            }
        }
        //重点在这里,调用Window的superDispatchKeyEvent(event)方法
        Window win = getWindow();
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        //如果View层次没有处理的话,就交给KeyEvent本身的dispatch方法,Activity的各种回调方法会被触发
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

在Activity中,先进行判断,之后调用Window的java superDispatchKeyEvent(event),将事件向下派发到View层次,如果View层次没有处理的话,会调用Activity中KeyEvent的各种回调方法。沿着主路线继续看:

<!-- Window.java -->
 /**
     * 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);

<!-- PhoneWindow.java -->
 @Override
    public boolean superDispatchKeyEvent(KeyEvent event) {
        return mDecor.superDispatchKeyEvent(event);
    }
<!-- DecorView.java -- >
  public boolean superDispatchKeyEvent(KeyEvent event) {
        // Give priority to closing action modes if applicable.
        //如果有设置ActionMode,则优先调用ActionMode
        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层。
        return super.dispatchKeyEvent(event);
    }

通过一系列的调用最终调用到java super.dispatchKeyEvent(event);,因为DecorView的父类是FrameLayout;所以事件从这里开始就正式进入View层次了。

我们来总结一下在DecorView中都做了哪些事情:

  1. java DecorView.dispatchKeyEvent(ev)方法中接受从ViewRootImpl传过来的事件,判断,分发至Activity或View处理,如果都没有处理的话,则执行PhoneWindow的相关方法。
  2. java Activity.dispatchKeyEvent(ev)方法中调用Window的处理方法,如果没有被处理的话,会触发Activity实现KeyEvent.Callback的相关方法。
  3. 通过Window->PhoneWindow->DecorView,事件再次传递到java DecorView.superDispatchKeyEvent(ev)方法中,直接调用父类的java dispatchKeyEvent(event)事件被传递到View层。

在DecorView层接收事件,再通过DecorView层将事件发下去。

View

相对于MotionEvent,KeyEvent的最大的不同点就是在View层的处理,其实以上的步骤同样适用于MotionEvent。有兴趣的童鞋可以从ViewRootImpl中的InputStage找到线索。
继续我们的主题,当事件传递到View层之后首先被触发的应该是java ViewGroup.dispatchKeyEvent(ev)方法:

<!--ViewGroup.java -- >
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        //KeyEvent一致性检测,忽略
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onKeyEvent(event, 1);
        }
        
        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;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
        }
        return false;
    }

这里我们可以看出对KeyEvent来说在View层次结构中,如果ViewGroup条件满足则会优先处理事件而不是先派发给其孩子view,这第一点是和MotionEvent最大的不同,在处理MotionEvent的时候ViewGroup是默认不处理的,只有当子View不处理的时候才会交给ViewGroup处理。接下来看看java View.dispatchKeyEvent(ev)

<! -- View.java -- >
 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;
        //调用onKeyListener,如果它非空且view是ENABLED状态,监听器优先触发
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }
        //调用KeyEvent.dispatch方法,并将view对象本身作为参数传递进去,view的各种callback方法在这里被触发
        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

这里可以看到处理的逻辑还是蛮清晰的,对ViewGroup进行进行Flag的判断,决定是自己处理还是向下派发,如果View是可用的并且有对应的监听器,则触发监听器,否则触发View实现的KeyEvent.Callback的回调。其中在View处理的时候有一个 mAttachInfo.mKeyDispatchState变量,这个类是KeyEvent中的DispatcherState,静态类,作用是在KeyEvent.dispatch()中处理并追踪事件。这里不过多介绍了,详情可以参考这篇文章
关于ViewGroup的flag可以参考这篇文章

我们来总结一下在View中都做了哪些事情:

  1. 在ViewGroup中判断Flag来决定是否处理事件,不处理则交给子View处理;
  2. 在View中先判断有没有监听器,有则触发,没有则触发KeyEvent.dispatch()回调;

到这里关于事件的派发流程其实就算是告一段落了,从ViewRootImpl到DecorView再到View层。接下来还有一些要说的那就是回调,也就是返回路线。

返回路线

通过上面的内容我们了解到了事件从ViewRootImpl传递到View层,但是如果到View层没有被消耗处理的话,那这个事件接下来应该怎么派发?
对于MotionEvent会传递到父布局,最终传递到Activity。那对于KeyEvent是怎么处理这种情况的那?

三个主要的方法

  1. View中的KeyEvent.dispatch()方法;
  2. Activity中的KeyEvent.dispatch()方法;
  3. DecorView中PhoneWindow的onKeyDown和onKeyUp;

先来看看KeyEvent.dispatch()方法都做了哪些事情:


    <!--KeyEvent.java -- >
    /**
     * 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: {
                mFlags &= ~FLAG_START_TRACKING;
                if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
                        + ": " + this);
                // 回调Callback接口的onKeyDown方法,View和Activity都是此接口的实现者
                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);
                }
                //回调Callback接口的onKeyUp方法,View和Activity都是此接口的实现者
                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进行了判断,分别调用Callback接口的实现类相对应的方法。其他的一下是之前说过的KeyEvent中的DispatcherState这个类对于事件的处理,本人也不是很清楚,简单来说就是对事件进行一些标记,跟踪之类的。

分别看一下View和Activity对于KeyEvent.Callback的实现方法:


    <!-- View.java -- >
    /**
     * Default implementation of {@link KeyEvent.Callback#onKeyDown(int, KeyEvent)
     * KeyEvent.Callback.onKeyDown()}: perform press of the view
     * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER}
     * is released, if the view is enabled and clickable.
     * <p>
     * Key presses in software keyboards will generally NOT trigger this
     * listener, although some may elect to do so in some situations. Do not
     * rely on this to catch software key presses.
     *
     * @param keyCode a key code that represents the button pressed, from
     *                {@link android.view.KeyEvent}
     * @param event the KeyEvent object that defines the button action
     */
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (KeyEvent.isConfirmKey(keyCode)) {// 只处理KEYCODE_DPAD_CENTER、KEYCODE_ENTER这2个按键
            if ((mViewFlags & ENABLED_MASK) == DISABLED) {
                return true;// 针对disabled View直接返回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事件
                // For the purposes of menu anchoring and drawable hotspots,
                // key events are considered to be at the center of the view.
                final float x = getWidth() / 2f;
                final float y = getHeight() / 2f;
                setPressed(true, x, y);// 标记pressed,你可能设置了View不同的background,这时候就会有所体现(比如高亮效果)
                checkForLongClick(0, x, y);// 启动View的long click检测
                return true;// 到达这一步就表示KeyEvent被处理掉了
            }
        }

        return false;
    }

     private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { // 必须得是LONG_CLICKABLE的View
            mHasPerformedLongPress = false;// 设置初始值

            if (mPendingCheckForLongPress == null) {// 只非空的时候才new一个
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,/ post一个Runnable,注意延迟是个差值,而不是delayOffset
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

     private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;

        @Override
        public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {// 当时间到了,此Runnable没被移除掉的话,并且这些条件都满足的时候,
                if (performLongClick(mX, mY)) {// 客户端定义的onLongClickListener监听器被触发
                    mHasPerformedLongPress = true;
                }
            }
        }

        public void setAnchor(float x, float y) {
            mX = x;
            mY = y;
        }

        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }
    }
    
     /**performLongClick()最终会调到这个方法
     * Calls this view's OnLongClickListener, if it is defined. Invokes the
     * context menu if the OnLongClickListener did not consume the event,
     * optionally anchoring it to an (x,y) coordinate.
     *
     * @param x x coordinate of the anchoring touch event, or {@link Float#NaN}
     *          to disable anchoring
     * @param y y coordinate of the anchoring touch event, or {@link Float#NaN}
     *          to disable anchoring
     * @return {@code true} if one of the above receivers consumed the event,
     *         {@code false} otherwise
     */
    private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {// 优先触发监听器
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {// 如果还没处理,显示ContextMenu如果定义了的话
            final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
            handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;// 返回处理结果
    }

    
    /**
     * Default implementation of {@link KeyEvent.Callback#onKeyUp(int, KeyEvent)
     * KeyEvent.Callback.onKeyUp()}: perform clicking of the view
     * when {@link KeyEvent#KEYCODE_DPAD_CENTER}, {@link KeyEvent#KEYCODE_ENTER}
     * or {@link KeyEvent#KEYCODE_SPACE} is released.
     * <p>Key presses in software keyboards will generally NOT trigger this listener,
     * although some may elect to do so in some situations. Do not rely on this to
     * catch software key presses.
     *
     * @param keyCode A key code that represents the button pressed, from
     *                {@link android.view.KeyEvent}.
     * @param event   The KeyEvent object that defines the button action.
     */
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (KeyEvent.isConfirmKey(keyCode)) {// 同onKeyDown,默认也只处理confirm key
            if ((mViewFlags & ENABLED_MASK) == DISABLED) {// 同样的逻辑,如果是DISABLED view,直接返回true表示处理过了
                return true;
            }
            if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
                setPressed(false); // 重置pressed状态

                if (!mHasPerformedLongPress) {// 长按没发生的话,
                    // This is a tap, so remove the longpress check
                    removeLongPressCallback();// 当up事件发生的时候,移除这些已经没用的callback
                    return performClick();// 调用单击onClick监听器
                }
            }
        }
        return false;// 其他所有的Key默认不处理
    }

    /**
     * Sets the pressed state for this view.
     *
     * @see #isClickable()
     * @see #setClickable(boolean)
     *
     * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
     *        the View's internal state from a previously set "pressed" state.
     */
    public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }

        if (needsRefresh) {
            refreshDrawableState();// 这行代码会刷新View的显示状态
        }
        dispatchSetPressed(pressed);
    }

接下来,看看Activity对应的onKeyDown,onKeyUp方法:


<! -- Activity.java -- >

    /**
     * Called when a key was pressed down and not handled by any of the views
     * inside of the activity. So, for example, key presses while the cursor
     * is inside a TextView will not trigger the event (unless it is a navigation
     * to another object) because TextView handles its own key presses.
     *
     * <p>If the focused view didn't want this event, this method is called.
     *
     * <p>The default implementation takes care of {@link KeyEvent#KEYCODE_BACK}
     * by calling {@link #onBackPressed()}, though the behavior varies based
     * on the application compatibility mode: for
     * {@link android.os.Build.VERSION_CODES#ECLAIR} or later applications,
     * it will set up the dispatch to call {@link #onKeyUp} where the action
     * will be performed; for earlier applications, it will perform the
     * action immediately in on-down, as those versions of the platform
     * behaved.
     *
     * <p>Other additional default key handling may be performed
     * if configured with {@link #setDefaultKeyMode}.
     *
     * @return Return <code>true</code> to prevent this event from being propagated
     * further, or <code>false</code> to indicate that you have not handled
     * this event and it should continue to be propagated.
     * @see #onKeyUp
     * @see android.view.KeyEvent
     */
    public boolean onKeyDown(int keyCode, KeyEvent event)  {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (getApplicationInfo().targetSdkVersion
                    >= Build.VERSION_CODES.ECLAIR) {
                event.startTracking(); // 标记追踪这个key event
            } else {
                onBackPressed();// 直接调用onBackPressed
            }
            return true; // 返回true表示被activity处理掉了
        }

        if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
            return false;
        } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
            Window w = getWindow();
            if (w.hasFeature(Window.FEATURE_OPTIONS_PANEL) &&
                    w.performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, keyCode, event,
                            Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {
                return true;
            }
            return false;
        } else {
            // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
            boolean clearSpannable = false;
            boolean handled;
            if ((event.getRepeatCount() != 0) || event.isSystem()) {
                clearSpannable = true;
                handled = false;
            } else {
                handled = TextKeyListener.getInstance().onKeyDown(
                        null, mDefaultKeySsb, keyCode, event);
                if (handled && mDefaultKeySsb.length() > 0) {
                    // something useable has been typed - dispatch it now.

                    final String str = mDefaultKeySsb.toString();
                    clearSpannable = true;

                    switch (mDefaultKeyMode) {
                    case DEFAULT_KEYS_DIALER:
                        Intent intent = new Intent(Intent.ACTION_DIAL,  Uri.parse("tel:" + str));
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        startActivity(intent);
                        break;
                    case DEFAULT_KEYS_SEARCH_LOCAL:
                        startSearch(str, false, null, false);
                        break;
                    case DEFAULT_KEYS_SEARCH_GLOBAL:
                        startSearch(str, false, null, true);
                        break;
                    }
                }
            }
            if (clearSpannable) {
                mDefaultKeySsb.clear();
                mDefaultKeySsb.clearSpans();
                Selection.setSelection(mDefaultKeySsb,0);
            }
            return handled;
        }
    }
    /**
     * Called when a key was released and not handled by any of the views
     * inside of the activity. So, for example, key presses while the cursor
     * is inside a TextView will not trigger the event (unless it is a navigation
     * to another object) because TextView handles its own key presses.
     *
     * <p>The default implementation handles KEYCODE_BACK to stop the activity
     * and go back.
     *
     * @return Return <code>true</code> to prevent this event from being propagated
     * further, or <code>false</code> to indicate that you have not handled
     * this event and it should continue to be propagated.
     * @see #onKeyDown
     * @see KeyEvent
     */
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (getApplicationInfo().targetSdkVersion
                >= Build.VERSION_CODES.ECLAIR) {// 同onKeyDown,2.0之后的版本
            if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                    && !event.isCanceled()) {
                onBackPressed();// 在这种情况下执行onBackPressed表示处理掉了
                return true;
            }
        }
        return false;
    }

还有的就是在DecorView中最下面的一句PhoneWindow的回调方法:


    <! -- PhoneWindow.java -- >
     /**
     * A key was pressed down and not handled by anything else in the window.
     *
     * @see #onKeyUp
     * @see android.view.KeyEvent
     */
    protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {
        /* ****************************************************************************
         * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES.
         *
         * If your key handling must happen before the app gets a crack at the event,
         * it goes in PhoneWindowManager.
         *
         * If your key handling should happen in all windows, and does not depend on
         * the state of the current application, other than that the current
         * application can override the behavior by handling the event itself, it
         * should go in PhoneFallbackEventHandler.
         *
         * Only if your handling depends on the window, and the fact that it has
         * a DecorView, should it go here.
         * ****************************************************************************/

        final KeyEvent.DispatcherState dispatcher =
                mDecor != null ? mDecor.getKeyDispatcherState() : null;
        //Log.i(TAG, "Key down: repeat=" + event.getRepeatCount()
        //        + " flags=0x" + Integer.toHexString(event.getFlags()));

        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN:
            case KeyEvent.KEYCODE_VOLUME_MUTE: {
                int direction = 0;
                switch (keyCode) {
                    case KeyEvent.KEYCODE_VOLUME_UP:
                        direction = AudioManager.ADJUST_RAISE;
                        break;
                    case KeyEvent.KEYCODE_VOLUME_DOWN:
                        direction = AudioManager.ADJUST_LOWER;
                        break;
                    case KeyEvent.KEYCODE_VOLUME_MUTE:
                        direction = AudioManager.ADJUST_TOGGLE_MUTE;
                        break;
                }
                // If we have a session send it the volume command, otherwise
                // use the suggested stream.
                if (mMediaController != null) {
                    mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
                } else {
                    MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy(
                            mVolumeControlStreamType, direction,
                            AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE
                                    | AudioManager.FLAG_FROM_KEY);
                }
                return true;
            }
            // These are all the recognized media key codes in
            // KeyEvent.isMediaKey()
            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            case KeyEvent.KEYCODE_MUTE:
            case KeyEvent.KEYCODE_HEADSETHOOK:
            case KeyEvent.KEYCODE_MEDIA_STOP:
            case KeyEvent.KEYCODE_MEDIA_NEXT:
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
            case KeyEvent.KEYCODE_MEDIA_REWIND:
            case KeyEvent.KEYCODE_MEDIA_RECORD:
            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
                if (mMediaController != null) {
                    if (mMediaController.dispatchMediaButtonEvent(event)) {
                        return true;
                    }
                }
                return false;
            }

            case KeyEvent.KEYCODE_MENU: {
                onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event);
                return true;
            }

            case KeyEvent.KEYCODE_BACK: {
                if (event.getRepeatCount() > 0) break;
                if (featureId < 0) break;
                // Currently don't do anything with long press.
                if (dispatcher != null) {
                    dispatcher.startTracking(event, this);
                }
                return true;
            }

        }

        return false;
    }
    
    /**
     * A key was released and not handled by anything else in the window.
     *
     * @see #onKeyDown
     * @see android.view.KeyEvent
     */
    protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) {
        final KeyEvent.DispatcherState dispatcher =
                mDecor != null ? mDecor.getKeyDispatcherState() : null;
        if (dispatcher != null) {
            dispatcher.handleUpEvent(event);
        }
        //Log.i(TAG, "Key up: repeat=" + event.getRepeatCount()
        //        + " flags=0x" + Integer.toHexString(event.getFlags()));

        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN: {
                final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE
                        | AudioManager.FLAG_FROM_KEY;
                // If we have a session send it the volume command, otherwise
                // use the suggested stream.
                if (mMediaController != null) {
                    mMediaController.adjustVolume(0, flags);
                } else {
                    MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy(
                            mVolumeControlStreamType, 0, flags);
                }
                return true;
            }
            case KeyEvent.KEYCODE_VOLUME_MUTE: {
                // Similar code is in PhoneFallbackEventHandler in case the window
                // doesn't have one of these.  In this case, we execute it here and
                // eat the event instead, because we have mVolumeControlStreamType
                // and they don't.
                getAudioManager().handleKeyUp(event, mVolumeControlStreamType);
                return true;
            }
            // These are all the recognized media key codes in
            // KeyEvent.isMediaKey()
            case KeyEvent.KEYCODE_MEDIA_PLAY:
            case KeyEvent.KEYCODE_MEDIA_PAUSE:
            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            case KeyEvent.KEYCODE_MUTE:
            case KeyEvent.KEYCODE_HEADSETHOOK:
            case KeyEvent.KEYCODE_MEDIA_STOP:
            case KeyEvent.KEYCODE_MEDIA_NEXT:
            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
            case KeyEvent.KEYCODE_MEDIA_REWIND:
            case KeyEvent.KEYCODE_MEDIA_RECORD:
            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
                if (mMediaController != null) {
                    if (mMediaController.dispatchMediaButtonEvent(event)) {
                        return true;
                    }
                }
                return false;
            }

            case KeyEvent.KEYCODE_MENU: {
                onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId,
                        event);
                return true;
            }

            case KeyEvent.KEYCODE_BACK: {
                if (featureId < 0) break;
                if (event.isTracking() && !event.isCanceled()) {
                    if (featureId == FEATURE_OPTIONS_PANEL) {
                        PanelFeatureState st = getPanelState(featureId, false);
                        if (st != null && st.isInExpandedMode) {
                            // If the user is in an expanded menu and hits back, it
                            // should go back to the icon menu
                            reopenMenu(true);
                            return true;
                        }
                    }
                    closePanel(featureId);
                    return true;
                }
                break;
            }

            case KeyEvent.KEYCODE_SEARCH: {
                /*
                 * Do this in onKeyUp since the Search key is also used for
                 * chording quick launch shortcuts.
                 */
                if (getKeyguardManager().inKeyguardRestrictedInputMode()) {
                    break;
                }
                if (event.isTracking() && !event.isCanceled()) {
                    launchDefaultSearch(event);
                }
                return true;
            }

            case KeyEvent.KEYCODE_WINDOW: {
                if (mSupportsPictureInPicture && !event.isCanceled()) {
                    getWindowControllerCallback().enterPictureInPictureModeIfPossible();
                }
                return true;
            }
        }
        return false;
    }

至此所有按键事件的处理就分析完毕了。最后我们来个整体的总结一下。

  1. View的各种KeyEvent.Callback接口早于Activity的对应接口被调用;
  2. 整个处理环节中只要有一处表明处理掉了,则处理结束,不在往下传递;
  3. 各种Callback接口的处理优先级低于监听器,也就是说各种onXXXListener的方法优先被调用。

总结

梳理一遍整体流程:

  1. ViewRootImpl中的WindowInputEventReceiver接受时间,排列分发。
  2. ViewRootImpl中的InputStage对事件进行顺序处理。
  3. 通过ViewPostImeInputStage将事件传递到DecorView中。
  4. DecorView进行判断当前状态,将事件传递给Activity或ViewGroup。
  5. Activity将事件传递给Window再传递给DecorView,然后传递到View层。
  6. ViewGroup先判断是否消耗,消耗则事件终止,不消耗则传递给包括的子View。
  7. View先判断是否有监听器,优先触发监听,没有则触发View实现KeyEvent.Callback方法。
  8. 如果View监听器和回调都返回false,事件往回传递至Activity。
  9. 执行Activity实现KeyEvent.Callback的相关方法。
  10. 如果Activity中仍返回false,不消耗,则继续往回返,传递至DecorView。
  11. 执行PhoneWindow的onKeyDown和onKeyUp。

整体的流程应该就是这样,如果有不对的地方欢迎批评指正,我会第一时间更改纠正

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

推荐阅读更多精彩内容