Android強行進階|按鍵事件&焦點事件攻略

對於Android手機APP普通開發者來講,KeyEvent接觸相對較少,相反接觸較多的應該是TouchEvent。而Android TV開發者對KeyEvent的接觸就很是頻繁。這也是手機應用和TV應用的主要區別:一個主要響應手指觸摸事件,一個響應遙控器按鍵事件。html

本文主要基於Android 9.0的源碼,踏着巨人的肩膀,進行分析的,我的能力有限,有誤請多多指正。篇幅也比較長,對於流程不感興趣的,能夠直接看文末總結。java

帶着疑問學習本文:node

  1. KeyEvent在Activity、View層次結構是如何分發的?
  2. 咱們設置View的OnKeyListener和重寫onKeyDown()函數,可否同時生效?
  3. OnKeyListener與onKeyDown()的調用順序?
  4. 若是全部View都不消費事件,最後KeyEvent如何處理?

ViewRootImpl

因爲TouchEventKeyEvent等等相關Event的源頭是ViewRootImpl,因此須要先了解什麼是ViewRootImpl。在Android中,View之間的關係是以View樹的形式組織的,也是說,能夠經過根View查找到全部的 View,進而能夠判斷view樹是否有view消費相關事件。在Activity中,根View就是 DecorView。ViewRootImpl 自己並非一個View,而是View樹的管理者,ViewRootImpl能夠對View進行佈局(layout),測量(measure)和繪製(draw),以及分發事件。同時也是View和WindowManager的橋樑。算法

關於ViewRootImpl的更多信息windows

ViewPostImeInputStage

ViewRootImpl以不一樣的InputStage來管理不一樣的事件(Event),界面相關的事件(觸摸事件、按鍵事件、軌跡事件和手勢事件)由ViewRootImpl的內部類ViewPostImeInputStage來完成。ViewPostImeInputStage對象內的onProcess()函數是觸摸事件和焦點事件的源頭。bash

@Override
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);
        }
    }
}
複製代碼

這裏只關心processKeyEvent()函數對按鍵(焦點)事件作了什麼處理。app

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

    if (mUnhandledKeyManager.preViewDispatch(event)) {
        return FINISH_HANDLED;
    }

    //在View層次結構分發按鍵事件
    //若是View樹中有View消費事件dispatchKeyEvent()函數返回true,
    後續步驟再也不執行
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    //一些保護措施
    //在View層次結構不消費事件,判斷窗口是否有輸入事件或者已經中止和銷燬
    if (shouldDropInputEvent(q)) {
         return FINISH_NOT_HANDLED;
    }


    if (mUnhandledKeyManager.dispatch(mView, event)) {
        return FINISH_HANDLED;
    }
    //用來保存焦點事件方向
    int groupNavigationDirection = 0;
    //處理tab鍵,判斷焦點的方向
    if (event.getAction() == KeyEvent.ACTION_DOWN
            && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
        //metaStateHasModifiers()根據指定的meta狀態按下特定時返回true
         //若是按的是組合鍵則返回false
        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) {
        if (mView.dispatchKeyShortcutEvent(event)) {
            return FINISH_HANDLED;
        }
        if (shouldDropInputEvent(q)) {
            return FINISH_NOT_HANDLED;
        }
    }

    // 應用 fallback 策略
    // 具體實現見PhoneFallbackEventHandler中dispatchKeyEvent()方法
    // 主要是對媒體鍵,音量鍵,通話鍵等作處理
    if (mFallbackEventHandler.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (shouldDropInputEvent(q)) {
        return FINISH_NOT_HANDLED;
    }

    //View層次結構不處理KeyEvent,那麼變成了尋找焦點的過程
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        if (groupNavigationDirection != 0) {
            if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                return FINISH_HANDLED;
            }
        } else {
            if (performFocusNavigation(event)) {
                return FINISH_HANDLED;
            }
        }
    }
     return FORWARD;
}
複製代碼

processKeyEvent()函數中,咱們主要關心按鍵事件在View層次結構的分發(mView.dispatchKeyEvent(event))和函數最後自動處理焦點事件兩大過程。ide

KeyEvent在View層次結構的分發

在上一小節中,會調用ViewPostImeInputStage對象內processKeyEvent()函數,使KeyEvent在View的層次結構中分發。 其中mView是DecorView對象,並調用其dispatchKeyEvent()函數。函數

// DecorView 
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    final int keyCode = event.getKeyCode();
    final int action = event.getAction();
    final boolean isDown = action == KeyEvent.ACTION_DOWN;
    //第一次按下,處理panel快捷鍵
    if (isDown && (event.getRepeatCount() == 0)) {
        if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
            boolean handled = dispatchKeyShortcutEvent(event);
            if (handled) {
                return true;
            }
        }

        if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
            if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
                return true;
            }
        }
    }
    //重點分析的地方
    //當Window未destoryed且callback非null,
    //交給Window對象的callback處理
    if (!mWindow.isDestroyed()) {
        final Window.Callback cb = mWindow.getCallback();
        // mFeatureId < 0 表示是application的DecorView,好比Activity、Dialog
        final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)//事件派發給callback對象
                : super.dispatchKeyEvent(event);//派發給ViewGroup(View層次結構)
        if (handled) {
            return true;
        }
    }
    //只有View不消費事件,纔將事件交給Window對象
    return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
            : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
複製代碼

咱們知道Activity和Dialog都是實現了CallBack接口,所以這裏先分析KeyEvent在Activity中分發,再看看View層次結構分發。由於PhoneWindow是Window的惟一實現類,因此最後須要看看PhoneWindow的onKeyDown()onKeyUp()函數。佈局

//Activity.java

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

    final int keyCode = event.getKeyCode();
    //處理KEYCODE_MENU鍵,Activity有ActionBar且消費該按鍵
    if (keyCode == KeyEvent.KEYCODE_MENU &&
            mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
        return true;
    }

    Window win = getWindow();
    //這裏KeyEvent交給Window對象分發
    if (win.superDispatchKeyEvent(event)) {
        return true;
    }
    View decor = mDecor;
    if (decor == null) decor = win.getDecorView();
    
    //Window對象win不消費Key事件,則將事件交給KeyEvent自身
    return event.dispatch(this, decor != null
            ? decor.getKeyDispatcherState() : null, this);
}
複製代碼

經過後面的學習,能夠KeyEvent在Window對象的分發實際上是在View的層次結構分發。在View層次結構中不消費KeyEvent事件,那麼會交給KeyEvent自身處理,會調用Activity相關方法,如onKeyDown()

咱們知道PhoneWindow是Window的具體實現,因此看看PhoneWindow對象的superDispatchKeyEvent()函數。

//Window

/**
 * 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

@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
}
複製代碼

能夠看到,又回到DecorView對象的superDispatchKeyEvent()函數。

// DecorView

public boolean superDispatchKeyEvent(KeyEvent event) {
    //優先處理KEYCODE_BACK的事件
    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層次結構進行派發
    if (super.dispatchKeyEvent(event)) {
        return true;
    }
    
    //若是View層次結構不消費,且ViewRootImpl不爲空,
    //則在ViewRootImpl對象處理
    return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
}
複製代碼

View層次結構大概以下圖所示,這裏先看看KeyEvent在View層次結構的分發。

咱們知道,DecorView繼承至FrameLayout,而FrameLayout是ViewGroup的子類。 ViewGroup類的 dispatchKeyEvent()函數以下:

//ViewGroup

 @Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 1);
    }
    //ViewGroup是focused或者設置了具體的大小,則交給它實現
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        //在View中的實現
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        //mFocused表示當前ViewGroup中得到焦點或者包含焦點的View(子View)
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }

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

從上面代碼能夠看出,子View想要得到焦點,處理KeyEvent,須要設置focusable屬性爲true。KeyEvent會優先派發給符合條件的ViewGroup處理,而不是子View。mFocused.dispatchKeyEvent(event)中的dispatchKeyEvent()可能會迭代調用,由於子View也多是ViewGroup。

這裏看看View中事件分發。

//View

    /**
     * Dispatch a key event to the next view on the focus path. This path runs
     * from the top of the view tree down to the currently focused view. If this
     * view has focus, it will dispatch to itself. Otherwise it will dispatch
     * the next node down the focus path. This method also fires any key
     * listeners.
     *
     * @param event The key event to be dispatched.
     * @return True if the event was handled, false otherwise.
     */
    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;
        //若是咱們給View設置了OnKeyListener且View是ENABLED狀態,
        //則會回調咱們的了OnKeyListener
        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和View的dispatchKeyEvent()就構成了View層次結構的KeyEvent分發,且老是從樹根DecorView開始到具體的View。注意到此處在View不消費KeyEvent會調用KeyEvent.dispatch方法,在Activity也會調用該方法。

//KeyEvent
public final boolean dispatch(Callback receiver, DispatcherState state,
        Object target) {
    switch (mAction) {
        case ACTION_DOWN: {
            mFlags &= ~FLAG_START_TRACKING;
            //回調Callback對象receiver的onKeyDown函數,上文知道Activity和View都實現Callback
            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) {
                //reset state的內部狀態,也改變了KeyEvent的某些狀態
                state.handleUpEvent(this);
            }
            //回調Callback對象receiver的onKeyUp函數
            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;
}
複製代碼

在上面代碼中,能夠看到Callback對象的onKeyDown(),onKeyUp(),onKeyLongPress()函數被回調。而Activity和View是Callback接口的實現,所以調用Activity和View對應的方法。

先看看Activity對幾個方法的實現:

//Activity

public boolean onKeyDown(int keyCode, KeyEvent event)  {
    //處理返回鍵
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        if (getApplicationInfo().targetSdkVersion
                >= Build.VERSION_CODES.ECLAIR) {
            // 標記追蹤這個key event    
            event.startTracking();
            } else {
            //手機APP常在Activity重寫該方法,
            //要求用戶雙擊兩次來退出APP,而不是一次就退出APP
            onBackPressed();
        }
        return true;
    }

    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 if (keyCode == KeyEvent.KEYCODE_TAB) {
        return false;
    } else {
        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) {
                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;
    }
}

public boolean onKeyLongPress(int keyCode, KeyEvent event) {
    return false;
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getApplicationInfo().targetSdkVersion
            >= Build.VERSION_CODES.ECLAIR) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
    }
    return false;
}
複製代碼

而View中實現:

//View
public boolean onKeyDown(int keyCode, KeyEvent event) {
   //KEYCODE_DPAD_CENTER、KEYCODE_ENTER、
   //KEYCODE_SPACE、KEYCODE_NUMPAD_ENTER都返回ture,其餘返回false
    if (KeyEvent.isConfirmKey(keyCode)) {
        //當前View是DISABLED狀態直接返回false
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }

      if (event.getRepeatCount() == 0) {
            final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
                    || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
            if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
                final float x = getWidth() / 2f;
                final float y = getHeight() / 2f;
                if (clickable) {
                    //標記爲Pressed,例如根據狀態改變背景
                    setPressed(true, x, y);
                }
                //長按檢測
                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) {
                removeLongPressCallback();
                if (!event.isCanceled()) {
                    return performClickInternal();
                }
            }
        }
    }
    return false;
}
複製代碼

在DecorView的superDispatchKeyEvent()函數最後一行,若是View層次結構不消費事件,那麼會調用ViewRootImpl對象的dispatchUnhandledKeyEvent()函數,這裏主要是將未被消費的KeyEvent分發給註冊了處理全部KeyEvent的處理者(監聽器)

//ViewRootImpl.java

private final UnhandledKeyManager mUnhandledKeyManager
                     =new UnhandledKeyManager();

public boolean dispatchUnhandledKeyEvent(KeyEvent event) {
    return mUnhandledKeyManager.dispatch(mView, event);
}

boolean dispatch(View root, KeyEvent event) {
        if (mDispatched) {
            return false;
        }
        View consumer;
        try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "UnhandledKeyEvent dispatch");
            mDispatched = true;
            //將未處理的KeyEvent進行分發,若是有View消費該事件,則返回該
            //View
            consumer = root.dispatchUnhandledKeyEvent(event);
            //用於追蹤該事件
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    int keycode = event.getKeyCode();
                    if (consumer != null && !KeyEvent.isModifierKey(keycode)) {
                        mCapturedKeys.put(keycode, new WeakReference<>(consumer));
                    }
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            return consumer != null;
        }
複製代碼

這裏的root是DecorView,但從參數的類型來講,這裏root.dispatchUnhandledKeyEvent(event)應該是在View中實現的。

//View.java

View dispatchUnhandledKeyEvent(KeyEvent evt) {
    if (onUnhandledKeyEvent(evt)) {
        return this;
    }
    return null;
}

boolean onUnhandledKeyEvent(@NonNull KeyEvent event) {
    if (mListenerInfo != null && mListenerInfo.mUnhandledKeyListeners != null) {
        //mListenerInfo經過棧的方式保存是否有View設置UnhandledKeyListener,若是有且消費事件,則該DecorView消費該事件
        for (int i = mListenerInfo.mUnhandledKeyListeners.size() - 1; i >= 0; --i) {
            if (mListenerInfo.mUnhandledKeyListeners.get(i).onUnhandledKeyEvent(this, event)) {
                return true;
            }
        }
    }
    return false;
}
複製代碼

經過上文代碼能夠知道,咱們能夠在View中添加addOnUnhandledKeyEventListener(OnUnhandledKeyEventListener)來監聽全部未被處理的KeyEvent。會在KeyEvent正常分發未被消費前,且早於Window的Callback被回調。

在DecorView的dispatchKeyEvent()函數,若是View層次結構不消費事件,那麼會交給PhoneWindow的onKeyDown()onKeyUp()函數。

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

    final KeyEvent.DispatcherState dispatcher =
            mDecor != null ? mDecor.getKeyDispatcherState() : null;
 Integer.toHexString(event.getFlags()));

    switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
        case KeyEvent.KEYCODE_VOLUME_DOWN:
        case KeyEvent.KEYCODE_VOLUME_MUTE: {
            if (mMediaController != null) {
                mMediaController.dispatchVolumeButtonEventAsSystemService(event);
            } else {
                getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(event,
                        mVolumeControlStreamType);
            }
            return true;
        }

        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.dispatchMediaButtonEventAsSystemService(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;
            if (dispatcher != null) {
                dispatcher.startTracking(event, this);
            }
            return true;
        }

    }

    return false;
}

protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) {
    final KeyEvent.DispatcherState dispatcher =
            mDecor != null ? mDecor.getKeyDispatcherState() : null;
    if (dispatcher != null) {
        dispatcher.handleUpEvent(event);
    }

    switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
        case KeyEvent.KEYCODE_VOLUME_DOWN: {
            if (mMediaController != null) {
                mMediaController.dispatchVolumeButtonEventAsSystemService(event);
            } else {
                getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(
                        event, mVolumeControlStreamType);
            }
            return true;
        }
        case KeyEvent.KEYCODE_VOLUME_MUTE: {
            getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(
                    event, AudioManager.USE_DEFAULT_STREAM_TYPE);
            return true;
        }

        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.dispatchMediaButtonEventAsSystemService(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) {
                        reopenMenu(true);
                        return true;
                    }
                }
                closePanel(featureId);
                return true;
            }
            break;
        }

        case KeyEvent.KEYCODE_SEARCH: {                /*

            if (isNotInstantAppAndKeyguardRestricted()) {
                break;
            }
            if ((getContext().getResources().getConfiguration().uiMode
                    & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_WATCH) {
                break;
            }
            if (event.isTracking() && !event.isCanceled()) {
                launchDefaultSearch(event);
            }
            return true;
        }

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

    return false;
}
複製代碼

從上面能夠看到,PhoneWindow也只對一些物理按鍵作了處理,若是PhoneWindow和View、Activity都沒有消費事件,那麼ViewPostImeInputStage對象經過系統算法自動尋找焦點了。

總結一下:

  1. 監聽器的優先級高於Callback的回調,也就是說OnKeyListener的函數優先Callback的onKeyDown等等函數的回調。
  2. View的Callback回調要早於Activity,Activity的回調早於PhoneWindow。
  3. 優先級高的消費KeyEvent,優先級低的再也不受理該事件。

下面經過時序圖對上文KeyEvent作一個總體流程的闡釋(雖然不能準備表達意思)

在圖表示在KeyEvent在DecorView開始不攔截最終在View的OnKeyListener或Callback對象被消費的過程。

參考文章

系統自動尋找焦點

回到夢開始的地方ViewPostImeInputStage對象的processKeyEvent()函數的末尾,在自動處理焦點的地方,會調用performFocusNavigation()函數來尋找下個得到焦點的View。

//ViewRootImpl.java

private boolean performFocusNavigation(KeyEvent event) {

    int direction = 0;
    //從下面代碼能夠看出,switch語句在此的主要做用是判斷焦點的方向
    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) {
        //分析一
        //mView在這裏是DecorView對象,查找出當前得到焦點的View
        View focused = mView.findFocus();
        if (focused != null) {
            //分析二
            //在當前得到焦點View經過指定方向搜索下一個得到焦點的View
            View v = focused.focusSearch(direction);
            if (v != null && v != focused) {
                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 true;
                }
            }

            //給DecorView最後一次處理焦點的機會
            if (mView.dispatchUnhandledMove(focused, direction)) {
                return true;
            }
        } else {
            //若是沒有View得到焦點
            if (mView.restoreDefaultFocus()) {
                return true;
            }
        }
    }
    return false;
}
複製代碼

在分析一處,經過DecorView對象mView來查找已得到焦點的View,findFocus()函數在ViewGroup和View都有實現,而DecorView繼承自ViewGroup。這裏其實進入了View的層次結果查找已得到焦點的View.

//ViewGroup.java
@Override
public View findFocus() {
    
    if (isFocused()) {
        return this;
    }

    if (mFocused != null) {
        //mFocused表示得到焦點的View,有多是ViewGroup,也有多是具體的View
        return mFocused.findFocus();
    }
    return null;
}

//View.java
public View findFocus() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}

public boolean isFocused() {
    return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
複製代碼

對於ViewGroup來講,若是自己得到焦點則直接返回自身便可,不然繼續經過mFocused.findFocus()函數繼續查找已得到焦點的View。具體的View是否得到焦點與ViewGroup的判斷條件是一致的,判斷PFLAG_FOCUSED標誌位,也就是咱們調用View的focuesabe=true,若是設置,則返回該View,不然返回null,表示沒有View得到焦點。

假設尋找到了已得到焦點的View,那麼下面就是尋找下一個得到焦點的View。也就是ViewPostImeInputStage對象的performFocusNavigation()函數分析二的代碼。因爲focused對象有多是ViewGroup,也有多是具體的View。一塊兒看看它兩的實現。

//View
//具體View的實現很是的簡單,若是有父視圖ViewGroup,則在俯視圖在尋找,
//不然返回null
public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
        return mParent.focusSearch(this, direction);
    } else {
        return null;
    }
}
//ViewGroup.java
@Override
public View focusSearch(View focused, int direction) {
    if (isRootNamespace()) { //判斷當前ViewGroup是否頂層View,即DecorView
        //調用FocusFinder實例findNextFocus進行查找
        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null) {
        //遞歸到頂層View
        return mParent.focusSearch(focused, direction);
    }
    return null;
}
複製代碼

經過遞歸方式,從內到外,到佈局最外層View,準確說是ViewGroup。而後調用FocusFinder的實例方法findNextFocus()來尋找下一個得到焦點的View。 FocusFinder類經過算法,根據當前得到焦點的View和按鍵方向來尋找下一個得到焦點的View。

//FocusFinder.java
public final View findNextFocus(ViewGroup root, View focused, int direction) {
    return findNextFocus(root, focused, null, direction);
}

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    View next = null;
    ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
    if (focused != null) {
        //尋找當前得到焦點view是否設置了指定方向的下一個得到焦點的View
        next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        effectiveRoot.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            //經過算法尋找最近可得到焦點的View
            next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}
複製代碼

findNextFocus()函數主要經過兩種方式來尋找指定方向的下一個得到焦。的View:咱們在XML佈局或者代碼設置特定方向得到焦點的View;經過算法來尋找特定方向能夠得到焦點最近的View。

對於方式一,findNextUserSpecifiedFocus()函數的實現以下:

//View.java
//主要做用是判斷當前得到焦點的View有沒有指定下一個得到焦點View的ID
View findUserSetNextFocus(View root, @FocusDirection int direction) {
    switch (direction) {
        case FOCUS_LEFT:
            if (mNextFocusLeftId == View.NO_ID) return null;
        return findViewInsideOutShouldExist(root, mNextFocusLeftId);
        case FOCUS_RIGHT:
            if (mNextFocusRightId == View.NO_ID) return null;
            return findViewInsideOutShouldExist(root, mNextFocusRightId);
        case FOCUS_UP:
            if (mNextFocusUpId == View.NO_ID) return null;
           return findViewInsideOutShouldExist(root, mNextFocusUpId);
        case FOCUS_DOWN:
            if (mNextFocusDownId == View.NO_ID) return null;
            return findViewInsideOutShouldExist(root, mNextFocusDownId);
        case FOCUS_FORWARD:
            if (mNextFocusForwardId == View.NO_ID) return null;
            return findViewInsideOutShouldExist(root, mNextFocusForwardId);
        case FOCUS_BACKWARD: {
            if (mID == View.NO_ID) return null;
            final int id = mID;
            return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
                @Override
                public boolean test(View t) {
                    return t.mNextFocusForwardId == id;
                }
            });
        }
    }
    return null;
}
//MatchIdPredicate匹配到相同ID會返回true
private View findViewInsideOutShouldExist(View root, int id) {
    if (mMatchIdPredicate == null) {
        mMatchIdPredicate = new MatchIdPredicate();
    }
    mMatchIdPredicate.mId = id;
    View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
    if (result == null) {
        Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
    }
    return result;
}
//從當前View開始,遍歷View的層次結構來的下一個得到焦點的View
public final <T extends View> T findViewByPredicateInsideOut(
    View start, Predicate<View> predicate) {
    View childToSkip = null;
    for (;;) {
        T view = start.findViewByPredicateTraversal(predicate, childToSkip);
        if (view != null || start == this) {
            return view;
        }

        ViewParent parent = start.getParent();
        if (parent == null || !(parent instanceof View)) {
            return null;
        }

        childToSkip = start;
        start = (View) parent;
    }
}
//匹配相同ID,返回該View
protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
        View childToSkip) {
    if (predicate.test(this)) {
        return (T) this;
    }
    return null;
}
複製代碼

在上面的相關方法,主要是經過在View的層次結構中去尋找到和指定id匹配的View。

那麼方式二,經過算法來尋找下一個焦點又是如何的呢?

//FocusFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
    int direction, ArrayList<View> focusables) {
    if (focused != null) {
        if (focusedRect == null) {
            focusedRect = mFocusedRect;
        }
        // fill in interesting rect from focused
        focused.getFocusedRect(focusedRect);
        root.offsetDescendantRectToMyCoords(focused, focusedRect);
    } else {
    if (focusedRect == null) {
        focusedRect = mFocusedRect;
        // make up a rect at top left or bottom right of root
        switch (direction) {
            case View.FOCUS_RIGHT:
            case View.FOCUS_DOWN:
                setFocusTopLeft(root, focusedRect);
                break;
            case View.FOCUS_FORWARD:
                if (root.isLayoutRtl()) {
                    setFocusBottomRight(root, focusedRect);
                } else {
                    setFocusTopLeft(root, focusedRect);
                }
                break;

            case View.FOCUS_LEFT:
            case View.FOCUS_UP:
                setFocusBottomRight(root, focusedRect);
                break;
            case View.FOCUS_BACKWARD:
                if (root.isLayoutRtl()) {
                    setFocusTopLeft(root, focusedRect);
                } else {
                    setFocusBottomRight(root, focusedRect);
                break;
            }
        }
    }
}
複製代碼

對FocusFinder就不做進一步分析了。感興趣同窗能夠本身看看源碼。

看到這裏,估計都很累了,咱們想了解也都知道了。

總結

全部的KeyEvent都會優先在View的層次結構分發,而後再經過自動尋找焦點來查找下一個得到焦點的View。這就是爲何在OnKeyListener或Callback相關回調方法返回true消費KeyEvent,下一個View沒法得到焦點。

KeyEvent在View的層次結構分發老是從外到裏,外層ViewGroup消費KeyEvent,內層的View是沒法得到焦點的。這就是爲何咱們不想要EditText彈出軟鍵盤,在根佈局設置focusable爲true的緣由。

OnKeyListener的調用要早於KeyEvent.Callback的調用,若是設置了OnKeyListener並消費了KeyEvent,那麼Callback相關函數不會再被調用。

另外,想要在KeyEvent分發前處理KeyEvent,例如TV開發處理特殊的按鍵,能夠修改PhoneWindowManagerinterceptKeyBeforeDispatching()函數。

最後的最後,能回答開頭的問題麼?

相關文章
相關標籤/搜索