Android 焦点分发机制梳理

ViewRootImpl

连接 DecorView, Window

image

DecorView -> Activity -> PhoneWindow -> DecorView 的来回绕一圈。

既然触摸事件已经到了 Activity.dispatchTouchEvent() 中了,为什么不直接分发给 DecorView ,而是要通过PhoneWindow 来间接发送呢?因为 Activity 不知道有 DecorView 这种奇怪的东西存在啊!不知道!但是,Activity 持有 PhoneWindow ,而 PhoneWindow 当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView 。你看,在 Android 中,Activity 并不知道自己的 Window 中有些什么,这样耦合性就很低了。

1
2
3
4
5
6
7
8
9
10
11
//Activity

public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (getWindow().superDispatchTouchEvent(ev)) {
//又把事件传到了Window中!
return true;
}
return onTouchEvent(ev);
//这就是为什么最后事件没有被消费的话,Activity会去处理的原因。
}

InputStage 策略

在 RootViewImpl 中的函数通道是各种策略(InputStage)的组合,各策略负责的任务不同,如SyntheticInputStage、ViewPostImeInputStage、NativePostImeInputStage 等等,这些策略以链表结构结构起来,当一个策略者没有消费事件时,就传递个下一个策略者。其中触摸和按键事件由 ViewPostImeInputStage 处理。

ViewPostImeInputStage 是 ViewRootImpl 的内部类,

分发 KeyEvent

  1. ViewPostImeInputStage

    -> onProcess (KeyEvent)

    -> processKeyEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    private int processKeyEvent(QueuedInputEvent q) {
    ...
    // Deliver the key to the view hierarchy.
    // mView 即 DecorView
    if (mView.dispatchKeyEvent(event)) {
    return FINISH_HANDLED;
    }
    ...
    // Handle automatic focus changes.
    if (performFocusNavigation(event)) {
    return FINISH_HANDLED;
    }
    }

    private boolean performFocusNavigation(KeyEvent event) {
    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 true;
    }
    }

    // Give the focused view a last chance to handle the dpad key.
    if (mView.dispatchUnhandledMove(focused, direction)) {
    return true;
    }
    } else {
    if (mView.restoreDefaultFocus()) {
    return true;
    }
    }
    }
    }
  2. DecorView

    -> dispatchKeyEvent

    1
    2
    3
    4
    5
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
    // Let the focused view and/or our descendants get the key first
    return super.dispatchKeyEvent(event) || executeKeyEvent(event);
    }
  3. ViewGroup

    -> dispatchKeyEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
    ...
    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) {
    if (mFocused.dispatchKeyEvent(event)) {
    return true;
    }
    }
    ...
    }

    如果父类(View)处理了(return true),则不往下传。或者 mFocused 不为空,先 mFocused 分发,如果 mFocused 没有处理(返回 true),则 DecorView 最终返回 false,交由 ViewRoomImpl 继续处理。

  4. View

    -> dispatchKeyEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public boolean dispatchKeyEvent(KeyEvent event) {
    ...
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
    && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
    return true;
    }
    ...
    }

    这里可以看出,要拦截焦点分发,可以:

    • 设置 OnKeyListener 监听
    • 重写 dispatchKeyEvent 方法并返回 true
  5. mView 分发焦点之后没有被处理,会先根据 KeyCode 为 direction 赋值,用于后续的寻找焦点。View focused = mView.findFocus() 会一层一层往下直到返回当前持有焦点的 View,如果 DecorView 的子 view 持有焦点(focused),则调用 focused 的 focusSearch 方法寻找下一个焦点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //ViewGroup
    @Override
    public View focusSearch(View focused, int direction) {
    if (isRootNamespace()) {
    // root namespace means we should consider ourselves the top of the
    // tree for focus searching; otherwise we could be focus searching
    // into other tabs. see LocalActivityManager and TabHost for more info.
    return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null) {
    return mParent.focusSearch(focused, direction);
    }
    return null;
    }

    //View
    public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
    return mParent.focusSearch(this, direction);
    } else {
    return null;
    }
    }

    可以看到 focusSearch 方法最终调用 FocusFinder.getInstance().findNextFocus(this, focused, direction) 来寻找下一个获取焦点的 View。FocusFinder 会优先通过 View 在 XML 布局设置的下一个焦点的 ID 来查找焦点。

参考

点击事件 InputStage

Android 焦点事件分发与传递机制

焦点 inputStage 相关