Android8.0 按鍵事件處理流程(一)

此處記錄按鍵事件從Framework到應用層的傳遞流程。WMS中接收到消息后,會調用ViewRootImpl中的dispatchInputEvent方法,

附上核心流程圖


Android按鍵事件流程

輸入事件認識

Android所有輸入事件都會封裝為InputEvent事件然后進行分發,InputEvent又分為兩種類型,實體按鍵事件(KeyEvent),觸摸事件(MotionEvent)。這些事件流入到上層之后才會進行分別進行處理。

下面源碼分析

按鍵事件流入

InputEvent就包含了KeyEvent,接下來就看該輸入事件如何傳遞和分別處理的,首先是ViewRootImpl的dispatchInputEvent方法

frameworks/base/core/java/android/view/ViewRootImpl.java

public void dispatchInputEvent(InputEvent event) {
    dispatchInputEvent(event, null);
}

public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
    SomeArgs args = SomeArgs.obtain();
    args.arg1 = event;
    args.arg2 = receiver; // 此處receiver為null
    Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args);
    msg.setAsynchronous(true);
    //發送MSG_DISPATCH_INPUT_EVENT消息
    mHandler.sendMessage(msg); 
}

mHandler是其內部類ViewRootHandler,接收到消息如下

final class ViewRootHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        ...
        case MSG_DISPATCH_INPUT_EVENT: {
            SomeArgs args = (SomeArgs)msg.obj;
            InputEvent event = (InputEvent)args.arg1;
            InputEventReceiver receiver = (InputEventReceiver)args.arg2;
            enqueueInputEvent(event, receiver, 0, true);
            args.recycle();
        } break;
    }
}

走enqueueInputEvent方法

void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {
    adjustInputEventForCompatibility(event);
    // 1. 將輸入事件event封裝為QueuedInputEvent
    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 {
        // 2. 追加新事件到mPendingInputEventTail上,形成事件鏈表
        last.mNext = q;
        mPendingInputEventTail = q;
    }
    mPendingInputEventCount += 1;
    if (processImmediately) {
        // 3. 處理輸入事件
        doProcessInputEvents();
    } else {
        scheduleProcessInputEvents(); 
    }
}

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;
        ...
        // 4. 分發QueuedInputEvent隊列中的所有事件
        deliverInputEvent(q); 
    }
    ...
}

InputEvent事件會形成一個事件鏈表,最后循環分發鏈表隊列中的事件

private void deliverInputEvent(QueuedInputEvent q) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
    }

    // 關于InputStage下面詳解,它表示了輸入事件的一個分發階段,eg: ime之前處理,ime處理,ime之后處理等
    InputStage stage;
    if (q.shouldSendToSynthesizer()) {
        stage = mSyntheticInputStage;
    } else {
        // 是否跳過鍵盤消息(IME),如果true,返回EarlyPostImeInputStage對象,否則返回NativePreImeInputStage對象
        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }

    if (stage != null) {
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}

NativePreImeInputStage是在ViewRootImpl中的setView()創建,而setview方法是在完成view繪制時調用的。

ViewRootImpl.java

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ...
            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;
        }
    }
}

先接著看deliverInputEvent方法中的stage.deliver(q);

abstract class InputStage {
    /**
     * Delivers an event to be processed.
     */
    public final void deliver(QueuedInputEvent q) {
        /// M: [ANR] Add for monitoring stage status. {
        ViewDebugManager.getInstance().debugInputStageDeliverd(this,
                System.currentTimeMillis());
        /// }
        // 當前事件還沒有處理,因此不包含FLAG_FINISHED標致
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
            forward(q);
            // 一般不會丟棄輸入事件
        } else if (shouldDropInputEvent(q)) {
            finish(q, false);
        } else {
            ViewDebugManager.getInstance().debugInputDispatchState(q.mEvent, this.toString());
            // 1. 執行apply方法,傳入onProcess的返回結果,該方法由子類重寫
            apply(q, onProcess(q));
        }
    }

    ...
    /**
     * Applies a result code from {@link #onProcess} to the specified event.
     */
    protected void apply(QueuedInputEvent q, int result) {
        if (result == FORWARD) {
            forward(q);
        } else if (result == FINISH_HANDLED) {
            finish(q, true);
        } else if (result == FINISH_NOT_HANDLED) {
            finish(q, false);
        } else {
            throw new IllegalArgumentException("Invalid result: " + result);
        }
    }

    /**
     * Called when an event is ready to be processed.
     * @return A result code indicating how the event was handled.
     */
    protected int onProcess(QueuedInputEvent q) {
        return FORWARD;
    }
    ...
}

上面提到的所有InputState都繼承自InputStage
如果不跳過IME消息,則inputStage為NativePreImeInputStage,其是由ViewPreImeInputStage,ImeInputStage,EarlyPostImeInputStage,NativePostImeInputStage,ViewPostImeInputStage,SyntheticInputStage作為嵌套參數構成的,所以調用NativePreImeInputStage的deliver(q),會依次調用到每個InputState的子類的onProcess()方法

這里先介紹下InputStage

InputStage

輸入事件的傳遞過程如下,每個前面處理事件的階段都有攔截傳遞的能力。

  1. NativePreImeInputStage 分發早于IME的InputEvent事件到NativeActivity中去處理, NativeActivity和普通acitivty的功能區別不大,只是很多代碼都在native層去實現,這樣執行效率更高,并且NativeActivity在游戲開發中很實用。 不支持觸摸事件。
  2. ViewPreImeInputStage 分發早于IME的InputEvent到View框架處理,會調用acitivity的所有view的onkeyPreIme方法,這樣就給View在輸入法處理key事件之前先得到消息并處理的機會。 不支持觸摸事件
  3. ImeInputStage 分發InputEvent到IME處理 ImeInputStage的onProcess方法會調用InputMethodManager的dispatchInputEvent方法處理消息。 不支持觸摸事件。
  4. EarlyPostImeInputStage 輸入法之后輸入事件就會流到該階段,此時 屏幕上有焦點的View會高亮顯示,用來提示用戶焦點所在。支持觸摸事件。
  5. NativePostImeInputStage 分發InputEvent事件到NativeActivity,為了讓IME處理完消息后能先于普通的Activity處理消息。此時支持觸摸事件。
  6. ViewPostImeInputStage 分發InputEvent事件到View框架,支持觸摸事件。
  7. SyntheticInputStage 未處理的InputEvent都會傳到這個階段,例如手機上的虛擬按鍵消息

所有InputStage類的構造方法都會傳入一個InputStage類的變量,這樣最終會形成流水線線式的處理結構,也就是采用了責任鏈模式,每經過一個InputStage對象的處理都會進行判斷,看是否還需要將 events繼續向前傳輸,如果需要就調用forward()函數讓該變量中存儲的下一個InputStage對象處理該events,如果不需要就調用finish()函數結束events的傳輸

我們所關心的View框架的輸入事件,故著重分析ViewPostImeInputStage

/**
     * Delivers post-ime input events to the view hierarchy.
     */
final class ViewPostImeInputStage extends InputStage {
    ...
    @Override
    protected int onProcess(QueuedInputEvent q) {
        if (q.mEvent instanceof KeyEvent) {
            // 處理按鍵事件
            return processKeyEvent(q); 
        } else {
            // else中處理觸摸事件,觸摸事件又會根據不同類型的觸摸做不同的處理,例如鼠標觸摸,軌跡球(Android很早的一種交互方式,現在沒有手機用)觸摸,一般的手指觸摸
            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);
            }
        }
    }
}

此處我們分析按鍵事件

private int processKeyEvent(QueuedInputEvent q) {
    // 轉為KeyEvent事件
    final KeyEvent event = (KeyEvent)q.mEvent; 

    // Deliver the key to the view hierarchy.
    // 1. 先由DecorView進行按鍵事件派發
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    ...
    int groupNavigationDirection = 0; 
    
    // 根據TAB和SHIFT鍵的按下來判斷焦點方向為向前還是向后
    if (event.getAction() == KeyEvent.ACTION_DOWN
        && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
        if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
            groupNavigationDirection = View.FOCUS_FORWARD;
        } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                                                  KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
            groupNavigationDirection = View.FOCUS_BACKWARD;
        }
    }

    // If a modifier is held, try to interpret the key as a shortcut.
    if (event.getAction() == KeyEvent.ACTION_DOWN
        && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
        && event.getRepeatCount() == 0
        && !KeyEvent.isModifierKey(event.getKeyCode())
        && groupNavigationDirection == 0) {
        // 交由DecorView處理快捷鍵分發
        if (mView.dispatchKeyShortcutEvent(event)) {
            return FINISH_HANDLED;
        }
        ...
    }
    // mFallbackEventHandler會處理系統的一些按鍵,針對的是所有窗口
    // Apply the fallback event policy.
    if (mFallbackEventHandler.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    ...
    // Handle automatic focus changes.
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        if (groupNavigationDirection != 0) {
            if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                return FINISH_HANDLED;
            }
        } else {
            // 2. 處理鍵盤的上下左右的焦點查找
            if (performFocusNavigation(event)) {
                return FINISH_HANDLED;
            }
        }
    }
    return FORWARD;
}

注釋1處,mView處理按鍵事件mView具體指的是? 如果是Activity和Dialog,mView就是DecorView,是所有view的根;如果是Toast,mView是id為com.android.internal.R.id.message,這點在Toast.makeText方法中可以看出。此處只分析Activity。

本文主要分析View框架的按鍵事件派發

按鍵事件派發

1. DecorView.dispatchKeyEvent方法

DecorView.java

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    final int action = event.getAction();
    final boolean isDown = action == KeyEvent.ACTION_DOWN;

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

    if (!mWindow.isDestroyed()) {
        // cb是Activiy或者Dialog,我們只分析Activity 
        // mFeatureId在installDecor是構造的DecorView傳入的為-1,故調用Activity的dispatchKeyEvent
        final Window.Callback cb = mWindow.getCallback();
        final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)  
            : super.dispatchKeyEvent(event);
        if (handled) {
            return true;
        }
    }

    return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
        : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}

2. Activity的dispatchKeyEvent

Activity.java

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

    // Let action bars open menus in response to the menu key prioritized over
    // the window handling it
    final int keyCode = event.getKeyCode();
    // 如果按鍵是menu事件,則先回調Actionbar的onMenuKeyEvent()事件處理,如果返回沒有處理才會繼續往下走
    if (keyCode == KeyEvent.KEYCODE_MENU &&
        mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
        return true;
    }
    
    Window win = getWindow();
    // 1. 調用Phonewindow的superDispatchKeyEvent,最終會調用到DecorView的dispatchKeyEvent方法中
    if (win.superDispatchKeyEvent(event)) {
        return true;
    }
    View decor = mDecor;
    if (decor == null) decor = win.getDecorView();
    // 2. 如果Phonewindow 分發后返回false,則交由KeyEvent派發事件,調用Activity的onKeyDown/Up()方法
    return event.dispatch(this, decor != null
                          ? decor.getKeyDispatcherState() : null, this);
}

繼續分析注釋1,看如何派發輸入事件到ViewGroup

PhoneWindow.java

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

發現又進入到DecorView,但這次調用的方法是superDispatchKeyEvent

DecorView.java

public boolean superDispatchKeyEvent(KeyEvent event) {
    // Give priority to closing action modes if applicable.
    // 對BACK按鍵做處理,如果存在ActionMode則先退出ActionMode,ActionMode相當于一個臨時的ActionBar,具體使用還是google吧,此處不是重點
    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;
        }
    }
    // 1. DecorView父類是FrameLayout,但其沒有實現dispatchKeyEvent方法,故該處調用的是ViewGroup的方法
    return super.dispatchKeyEvent(event);
}

注釋1會調用其父類ViewGroup的dispatchKeyEvent()方法

3. ViewGroup的dispatchKeyEvent

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 1);
    }
    // 1. 如果viewgroup獲得焦點且邊界確定,則調用父view的也就是View的dispatchKeyEvent
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
        == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
               == PFLAG_HAS_BOUNDS) {
        // 2. 交由獲取焦點的子view進行按鍵事件的派發
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }

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

注釋2中mFocused存在于每個ViewGroup,其標識了ViewGroup的直接子View是否擁有或者包含焦點,通過mFocused.dispatchKeyEvent即可遞歸調用找到最終獲取焦點的View,然后調用該View的dispatchKeyEvent()方法,如注釋1.

4. View的dispatchKeyEvent

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;
    // 1. 當對view設置了OnKeyListener,且該view處于enabled狀態,則調用OnKeyListener的onKey()方法
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }
    // 2. KeyEvent派發事件,receiver為view,會回調View的onKeyDown/Up()方法
    if (event.dispatch(this, mAttachInfo != null
                       ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }

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

注釋1中對View是否設置了OnKeyListener做了判斷,如果設置,則先回調onKey()方法

注釋2,在onKey()返回false情況下會通過KeyEvent的dispatch方法調用View的onKeyDown/Up()方法。

KeyEvent的dispatch(),該方法在View#dispatchKeyEvent()方法中,如果onKey()返回false會調用,參數傳入的是View;該方法也會在Activity#dispatchKeyEvent()方法中win.superDispatchKeyEvent()方法返回false后進行調用,只不過參數傳入的是Activity,最終會調用Activity或者View的onKeyDown/Up()方法。

KeyEvent.java

public final boolean dispatch(Callback receiver, DispatcherState state,
            Object target) {
    switch (mAction) {
        case ACTION_DOWN: {
            mFlags &= ~FLAG_START_TRACKING; 
            // 1. 執行Activity或者View的onKeyDown()方法
            boolean res = receiver.onKeyDown(mKeyCode, this); 
            if (state != null) {
                if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
                    state.startTracking(this, target);
                } else if (isLongPress() && state.isTracking(this)) {
                    try {
                        if (receiver.onKeyLongPress(mKeyCode, this)) {
                            state.performedLongPress(this);
                            res = true;
                        }
                    } catch (AbstractMethodError e) {
                    }
                }
            }
            return res;
        }
        case ACTION_UP:
            if (state != null) {
                state.handleUpEvent(this);
            }
            // 2. 執行Activity或者View的onKeyUp()方法
            return receiver.onKeyUp(mKeyCode, this); 
       ...
    }
    return false;
}

receiver可能是Activity對象,也可能是view對象,具體情況具體分析

View對象的onKeyDown(),onKeyUp()

public boolean onKeyDown(int keyCode, KeyEvent event) {
    // isConfirmKey中會對keycode判斷是否是KEYCODE_DPAD_CENTER,KEYCODE_ENTER,KEYCODE_SPACE,KEYCODE_NUMPAD_ENTER幾個表示確定的鍵,也就是可以觸發點擊作用的鍵
    if (KeyEvent.isConfirmKey(keyCode)) {
        // 如果view處于DISABLED狀態,則直接返回true
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }

        if (event.getRepeatCount() == 0) {
            // Long clickable items don't necessarily have to be clickable.
            // View的CLICKABLE 和 LONG_CLICABLE是獨立的,互不影響
            final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
                || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
            if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
                // 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;
                if (clickable) {
                    // 設置按下狀態,比如更換view顏色,切換圖片等
                    setPressed(true, x, y);
                }
                // 該方法做的事情是發送一個可以判斷長按時間的演示runnable,時間一到則執行長按操作
                checkForLongClick(0, x, y);
                return true;
            }
        }
    }

    return false;
}

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

            if (!mHasPerformedLongPress) {
                // This is a tap, so remove the longpress check
                // 抬起時移除長按消息即可,如果沒有觸發長按,則長按消息會被移除
                removeLongPressCallback();
                if (!event.isCanceled()) {
                    // 回調OnClickListener的onClick()方法
                    return performClick();
                }
            }
        }
    }
    return false;
}

如果View設置了onClickListener()方法,則按下enter鍵后抬起時會執行onClick()方法

如果View的onKeyDown/Up()也返回false,則dispatchKeyEvent()方法會一直返回至Activity中,交由其onKeyDown/Up()處理

Activity對象的onKeyDown(),onKeyUp()

public boolean onKeyDown(int keyCode, KeyEvent event)  {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        if (getApplicationInfo().targetSdkVersion
            >= Build.VERSION_CODES.ECLAIR) {
            // >= android2.1則跟蹤按鍵傳遞過程
            event.startTracking();
        } else {
            // android2.1之前按下返回鍵直接返回
            onBackPressed();
        }
        return true;
    }

    ...
        return handled;
    }
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
    // 如果android版本>= 2.1 回調onBackPressed()方法退出Activity
    if (getApplicationInfo().targetSdkVersion
        >= Build.VERSION_CODES.ECLAIR) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
            && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
    }
    return false;
}

如果Activity里面的任何view、布局都沒有處理按鍵,就會傳遞到Activity的onKeyDown,onKeyUp。比如,當在EditText中輸入文字時,Activity的onKeyDown,onKeyUp不會接收到按鍵事件,因為EditText有自己的處理按鍵事件的方法,如果此時把焦點從EditText移走,onKeyDown,onKeyUp就會接收到按鍵事件。

Activity中onKeyDown/Up()也不做處理返回false時,事件一路返回至DecorView的dispatchKeyEvent()方法中,此時繼續調用PhoneWindow的onKeyDown/Up()方法

PhoneWindow的onKeyDown/Up()

PhoneWindow.java

protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {

    final KeyEvent.DispatcherState dispatcher =
        mDecor != null ? mDecor.getKeyDispatcherState() : null;

    switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
        case KeyEvent.KEYCODE_VOLUME_DOWN:
        case KeyEvent.KEYCODE_VOLUME_MUTE: {
            // If we have a session send it the volume command, otherwise
            // use the suggested stream.
            if (mMediaController != null) {
                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;
                }
                mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
            } else {
                MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(
                    event, mVolumeControlStreamType, false);
            }
            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;
}

onKeyDown/onKeyUp方法主要針對當前獲得焦點的窗口對一些特殊按鍵進行處理,包括音量+/-,多媒體控制按鍵,MENU,BACK

總結

按鍵事件從Framework層到View框架整體流程如流程圖,我們主要關心是Activity,ViewGroup和View的事件派發。

Activity可以通過dispatchKeyEvent()將KeyEvent派發給ViewGroup直到找到獲取焦點的View(當然可能就是ViewGroup獲取焦點),獲取焦點的View先去判斷OnKeyListener存在與否,存在回調onKey(),如果不存在或者返回false,則回調其onKeyDown/Up()方法,onClick()方法在onKeyUp()方法中會進行回調,此時如果還是返回false,則Activity中的onKeyDown/Up()方法得以調用,最后沒有處理則交給PhoneWindow的onKeyDown/Up()。我們也可以通過重寫對應方法來達到事件消費,也就是不繼續走剩余事件傳遞流程。

注意:PhoneFallbackEventHandler在ViewRoolImpl中提到過,其也是對特殊按鍵進行處理,但是那是針對所有的窗口,包括當前獲得焦點的窗口,而PhoneWindow只針對當前獲得焦點的窗口。PhoneFallbackEventHandler是在使用代碼如下

ViewRootImpl.java

private int processKeyEvent(QueuedInputEvent q) {
    ...
    // Apply the fallback event policy. mFallbackEventHandler就是PhoneFallbackEventHandler
    if (mFallbackEventHandler.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    ...
}

PhoneFallbackEventHandler

public boolean dispatchKeyEvent(KeyEvent event) {
    final int action = event.getAction();
    final int keyCode = event.getKeyCode();

    if (action == KeyEvent.ACTION_DOWN) {
        return onKeyDown(keyCode, event);
    } else {
        return onKeyUp(keyCode, event);
    }
}
boolean onKeyDown(int keyCode, KeyEvent event) {
    final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState();

    switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
        case KeyEvent.KEYCODE_VOLUME_DOWN:
        case KeyEvent.KEYCODE_VOLUME_MUTE: {
            MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(
                    event, AudioManager.USE_DEFAULT_STREAM_TYPE, false);
            return true;
        }


        case KeyEvent.KEYCODE_MEDIA_PLAY:
        case KeyEvent.KEYCODE_MEDIA_PAUSE:
        case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
            /* Suppress PLAY/PAUSE toggle when phone is ringing or in-call
             * to avoid music playback */
            if (getTelephonyManager().getCallState() != TelephonyManager.CALL_STATE_IDLE) {
                return true;  // suppress key event
            }
        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:
        case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: {
            handleMediaKeyEvent(event);
            return true;
        }

        case KeyEvent.KEYCODE_CALL: {
        ...
    }
}

主要是針對音量鍵,媒體相關,Call和Camera鍵進行處理。

特殊按鍵處理

第一次攔截在interceptKeyBeforeQueueing中,
result &= ~ACTION_PASS_TO_USER或者0,將會攔截事件,不在向下傳遞



第二次在攔截在interceptKeyBeforeDispatching,
return -1,事件將被攔截不在向下傳遞


例如對Home鍵做操作,由于Home并不會下發至View,故應用是無法監聽,但我們可以在interceptKeyBeforeDispatching中對其進行客制化,例如將其remap返回鍵KEYCODE_BACK下發,這時PhoneWindowManager就會收到被remap后的KEYCODE_BACK,進而最后交由View框架處理.
PhoneWindowManager.java

public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) {
        final boolean keyguardOn = keyguardOn();
        final int keyCode = event.getKeyCode();
        final int repeatCount = event.getRepeatCount();
        final int metaState = event.getMetaState();
        final int flags = event.getFlags();
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        ......
        if (keyCode == KeyEvent.KEYCODE_HOME) {
            // If we have released the home key, and didn't do anything else
            // while it was pressed, then it is time to go home!
            if (!down) {
                // wangyannan begin
                    Log.i("wangyannan","foreground");
                    /**
                    *通過inputManager注入KEYCODE_BACK事件,相當于在按home鍵時接收到的為back事件
                    */
                    InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE);
                    KeyEvent ke1 = new KeyEvent(event.getDownTime(), event.getEventTime(),
                    KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, event.getRepeatCount());
                    KeyEvent ke2 = new KeyEvent(event.getDownTime(), event.getEventTime(),
                    KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK, event.getRepeatCount());                                     
                    inputManager.injectInputEvent(ke1, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
                    inputManager.injectInputEvent(ke2, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
                    return -1;
                }
                //wangyannan end
                cancelPreloadRecentApps();

                ......
                handleShortPressOnHome();
                return -1;//返回-1則不響應home鍵
            }
            ......
        }
}
//wangyannan begin

例如某個Activity監聽按鍵,對HOME鍵做處理
XXXActivity.java

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    switch (keyCode){
        case KeyEvent.KEYCODE_BACK:
            // 如果該條件成立,則為系統原本的按鍵事件,否則為重定向的時間
            if(event.getFlags()!=0&&event.getDeviceId()!=-1&&event.getSource()!=0){
                //實際接收到的back事件
                Toast.makeText(this,"沒有重定向",Toast.LENGTH_SHORT).show();
            }else{
                //接受到的back事件為代碼中注入的back事件
                Toast.makeText(this,"重定向",Toast.LENGTH_SHORT).show();
            }
            return ture;
    }
    return super.onKeyDown(keyCode, event);
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372