Android按鍵分發流程之java層按鍵傳遞

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的層次結構中是如何分發的
DecorView的按鍵分發

接下來來看這裏先看看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

<!-- PhoneWindow.java -->
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
} 

<!-- PhoneWindow.DecorView -->
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.java
public 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.java
public 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章