Android事件全流程介紹

轉載請注明出處:http://www.lxweimin.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閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容