Android控件系统(八)——按键事件分发

474 阅读12分钟

Android版本:7.0(API27)

[TOC]


  按键事件分发需要根据控件树对焦点的管理进行事件分发,那控件树是如何管理焦点的呢?就是指通过视图的根View(例如Activity的DecorView)如何能找到控件树中当前获取焦点的控件。我们将核心内容分为如下三部分:

  • 控件树对焦点的管理;
  • 按键事件分发;
  • 下一个焦点控件的查找;

控件焦点体系建立

我们通过View.requestFoucs()的实现来揭示控件树对焦点的管理方式。

public final boolean requestFocus() {
    return requestFocus(View.FOCUS_DOWN);
}

public final boolean requestFocus(int direction) {
    return requestFocus(direction, null);
}

public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    return requestFocusNoSearch(direction, previouslyFocusedRect);
}
  • direction:表示焦点的寻找方向。当控件是ViewGroup时将会从左上角开始沿着这个方向查找可以获取焦点的子控件。本例只分析View,所以该参数并无任何效果;
  • previouslyFocusedRect:之前拥有焦点的控件的位置;

获取焦点条件判断

private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    // need to be focusable
    // 1.控件可见并且可以获得焦点
    if ((mViewFlags & FOCUSABLE) != FOCUSABLE
            || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }

    // need to be focusable in touch mode if in touch mode
    // 2.在触摸模式下可以获取焦点
    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
           return false;
    }

    // need to not have any parents blocking us
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }

    /* 4.调用handleFocusGainInternal使此控件获取焦点 */
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

标记3:
  如果该View的任意一父类控件(父亲、爷爷、祖爷爷等等)的mGroupFlags设置了FOCUS_BLOCK_DESCENDANTS时,则阻止该控件获取焦点。hasAncestorThatBlocksDescendantFocus会沿着控件树一路回溯到整个控件树的根控件并逐一检查mGroupFlags的设置。
  mGroupFlags的设置可以有三种取值,其中一种为FOCUS_BLOCK_DESCENDANTS。当父控件的这一特性取值为FOCUS_BLOCK_DESCENDANT时,父控件将会阻止其子控件、孙子控件等获取焦点。在介绍ViewGroup.requestFocus()时会详细介绍。
标记4:
  调用handleFocusGainInternal使此控件获取焦点

获取焦点

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " requestFocus()");
    }

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        // 1
        mPrivateFlags |= PFLAG_FOCUSED;

        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

        if (mParent != null) {
            // 2
            mParent.requestChildFocus(this, this);
            updateFocusedInCluster(oldFocus, direction);
        }

        if (mAttachInfo != null) {
            // 3
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }

        // 4
        onFocusChanged(true, direction, previouslyFocusedRect);
        // 5
        refreshDrawableState();
    }
}

handleFocusGainInternal让该View获取焦点:
标记1:
  设置mPrivateFlags标志为PFLAG_FOCUSED,表示该控件获取焦点。
标记2:
  将这一变化通知其父控件,以将焦点从上一个焦点控件夺走并转移到本控件身上,并在ViewRootImpl中触发一次"遍历"以便对控件树进行重绘。
标记3:
  触发通过如下代码添加的监听

view.getViewTreeObserver().addOnGlobalFocusChangeListener(this); 

标记4:
  触发mOnFocusChangeListener的监听
标记5:
  刷新drawable获取焦点时的状态

  上述所有的工作都是在改变旧的焦点体系的状态,设置新的焦点体系;PFLAG_FOCUSED是一个控件拥有焦点的最直接体现,然而这并不是焦点管理的全部。这一标记仅仅体现了焦点在个体上的特性;而mParent.requestChildFocus则体现了焦点在控件树级别上的特性。

控件树中的焦点体系

mParent.requestChildFocus是一个定义在ViewParent接口中的方法,其实现者为ViewGroup和ViewRootImpl。
ViewGroup的实现目的有两个:

  • 将焦点从上一个焦点控件手中夺走,即将PFLAG_FOCUSED标记从控件的mPrivateFlags中移除;
  • 将这一操作继续向控件树的根部进行回溯,直到ViewRootImpl,ViewRootImpl的requestChildFocus会将焦点控件保存起来备用,并引发一次“遍历”使得整个控件树进行重绘;

ViewGroup.requestChildFocus

@Override
public void requestChildFocus(View child, View focused) {
    if (DBG) {
        System.out.println(this + " requestChildFocus()");
    }
    if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
        return;
    }

    // Unfocus us, if necessary
    // 1.
    super.unFocus(focused);

    // We had a previous notion of who had focus. Clear it.
    if (mFocused != child) {
        // 2.
        if (mFocused != null) {
            mFocused.unFocus(focused);
        }

        mFocused = child;
    }
    // 3.
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

标记1:
  如果之前的焦点控件就是这个ViewGroup,则释放其焦点;
标记2:

  • 拥有焦点的旧控件mFocused与期望获取焦点的新控件child不为同一个控件,且mFocused不为null,则释放旧控件mFocused的焦点;
  • 将child赋值给mFocused,注:mFocused指向的是新拥有焦点的控件或者新拥有焦点的控件的父亲、爷爷、祖爷爷等;

标记3:
将这一操作继续向控件树的根部回溯;

  此方法中的mFocused是一个View类型的变量,它是控件树焦点管理的核心所在,围绕着mFocused,ViewGroup.requestChildFocus方法包含了新的焦点体系的建立过程,以及旧的焦点体系的销毁过程。

新焦点体系建立

  新的焦点体系的建立过程是通过在ViewGroup.requestChildFocus方法的回溯过程中进行mFocused = child这一赋值操作完成的。当回溯完成后,mFocused = child将会建立起一个单向链表,使得从根控件开始通过mFocused成员可以沿着这一单向链表找到实际拥有焦点的控件,即实际拥有焦点的控件位于这个单向链表的尾端,如下图所示:

image

旧焦点体系销毁

  旧的焦点体系的销毁过程则是通过在回溯过程中调用mFocused.unFocus完成的。unFocus有ViewGroup和View两种实现。首先看一下ViewGroup.unfocus的实现:

void unFocus(View focused) {
    if (DBG) {
        System.out.println(this + " unFocus()");
    }
    if (mFocused == null) {
        super.unFocus(focused);
    } else {
        mFocused.unFocus(focused);
        mFocused = null;
    }
}

  可见ViewGroup.unfocus将unfocus调用沿着mFocused所描述的链表沿着控件树向下遍历,知道焦点的实际拥有者。焦点的实际拥有者会调用View.unFocus(),它会将PFLAG_FOCUSED移除,当然也少不了更新DrawableState以及onFocusChanged()方法的调用。 View.unfocus()

void unFocus(View focused) {
    if (DBG) {
        System.out.println(this + " unFocus()");
    }

    clearFocusInternal(focused, false, false);
}

void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
        mPrivateFlags &= ~PFLAG_FOCUSED;

        if (propagate && mParent != null) {
            mParent.clearChildFocus(this);
        }

        onFocusChanged(false, 0, null);
        refreshDrawableState();

        if (propagate && (!refocus || !rootViewRequestFocus())) {
            notifyGlobalFocusCleared(this);
        }
    }
}

重绘控件树

控件树最终会调用根控件的requestChildFocus,这里的根控件其实就是ViewRootImpl

 public void requestChildFocus(View child, View focused) {
    if (DEBUG_INPUT_RESIZE) {
        Log.v(mTag, "Request child focus: focus now " + focused);
    }
    checkThread();
    scheduleTraversals();
}

前面的文章我们分析过View的绘制流程,ViewRootImpl.scheduleTraversals会触发整个控件树重新绘制,这样整个焦点变化的过程就完成了。

总结

image

  以上图所示的控件树的焦点状态为例来描述旧有焦点体系的销毁以及新焦点体系的建立过程。当View2-1-1通过View.requestFocus()尝试获取焦点时,首先会将PLFAGL_FOCUSED标记加入其mPrivateFlags成员中以声明其拥有焦点。然后调用ViewGroup2-1的requestChildFocus(),此时ViewGroup2-1会尝试通过unFocus()销毁旧的焦点体系,但是由于其mFocused为null,它无法进行销毁,于是它将其mFocused设置为View2-1-1后将requestChildFocus()传递给ViewGroup2。此时ViewGroup2的mFocused指向ViewGroup2-2,于是调用ViewGroup2-2的unFocus()销毁旧的焦点体系。ViewGroup2-2的unFocus()将次操作传递给View2-2-2的unFocus()以移除View2-2-2的PLFAGL_FOCUSED标记,并将其mFocused置为null。回到ViewGroup2的requestChildFocus()方法后,ViewGroup2将其mFocused重新指向到ViewGroup2-1。在这些工作完成后,就形成了新的焦点体系,如下图所示
image

  总而言之,控件树的焦点管理分为两部分:

  1. 描述个体级别的焦点状态的PLFAGL_FOCUSED标记,用于表示一个控件是否拥有焦点;
  2. 描述控件树级别的焦点状态的ViewGroup.mFocused成员,用于提供一条链接控件树的根控件到实际拥有焦点的子控件的单向链表。这条链表提供了再控件树中快速查找焦点控件的简便办法。另外,由于焦点的排他性,当一个控件通过requestFocus()获取焦点以创建新的焦点体系时伴随着旧有焦点体系的销毁过程;

ViewGroup.requestFocus

public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    int descendantFocusability = getDescendantFocusability();

    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            return took ? took : super.requestFocus(direction, previouslyFocusedRect);
        }
        default:
            throw new IllegalStateException("descendant focusability must be "
                    + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
                    + "but is " + descendantFocusability);
    }
}

根据descendantFocusability不同的取值,执行的处理方式不同:

  • FOCUS_BLOCK_DESCENDANTS:阻止所有子控件获取焦点,调用super.requestFocus尝试自己获取焦点;
  • FOCUS_BEFORE_DESCENDANTS:ViewGroup优先于子控件获取焦点,如果ViewGroup不满足获取焦点的条件,则通过调用onRequestFocusInDescendants方法将获取焦点的请求转发给子控件;
  • FOCUS_AFTER_DESCENDANTS:与FOCUS_BEFORE_DESCENDANTS相反,子控件优先ViewGroup获取焦点,如果子控件不满足获取焦点的条件,则ViewGroup尝试自己获取焦点;
protected boolean onRequestFocusInDescendants(int direction,
            Rect previouslyFocusedRect) {
    int index;
    int increment;
    int end;
    int count = mChildrenCount;
    if ((direction & FOCUS_FORWARD) != 0) {
        index = 0;
        increment = 1;
        end = count;
    } else {
        index = count - 1;
        increment = -1;
        end = -1;
    }
    final View[] children = mChildren;
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            if (child.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
        }
    }
    return false;
}

onRequestFocusInDescendants其实是一种最简单的焦点查找算法。它按照direction方向,在mChildren列表中依次调用子控件的requestFocus()方法,直到有一个子控件获取焦点。另外,需要注意子控件也可能是一个ViewGroup,所以这里将会有一个递归的过程。

下一个焦点控件查找

  当一个控件获取焦点后,用户往往会通过方向键来移动焦点,这时控件系统需要在控件树中指定的方向上寻找距离当前控件最近的一个控件,并将焦点赋予它。与ViewGroup.onRequestFocusInDescendants()方法按照控件在mChildren数组中的顺序查找不同,这一查找依赖于控件在窗口中的位置。这一工作由View.focusSearch()方法完成。

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

又会调用父类的focusSearch,父类就是ViewGroup

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

如果此ViewGroup不是根控件,则继续向控件树的根部回溯,直到根控件,然后使用FocusFinder的findNextFocus方法查找下一个焦点。这个方法三个参数的意义如下:

  • this:即root。findNextFocus方法通过这个参数获取整个控件树中所有的候选控件。
  • focused:当前拥有焦点的控件。findNextFocus方法会以这个控件所在的位置开始查找。
  • direction:查找的方向。
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) {
        next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        effectiveRoot.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

  findNextFocus会首先尝试通过findNextUserSpecifiedFocus获取由开发者设置的下一个焦点控件。有时候控件系统内置的焦点查找算法并不能满足开发者的需求,因此需要开发者可以通过View.setNextFocusXXXId()方法或XML中的nextFocusXXX属性设置此控件的下一个可获取焦点的控件Id。其中XXX可以是Left、Right、Top、Bottom和Forword,分别用来设置不同方向上的下一个焦点控件。
  倘若开发者在指定方向上没有设置下一个焦点控件,则通过内置的搜索算法进行查找。这个内置算法会首先将控件树中所有可获取焦点的控件添加到一个名为focusables的列表中,并以这个列表作为焦点控件的候选集合。这样做的目的并不仅仅是提高效率,更重要的是这个列表打破了控件在控件树中的层次关系。它在一定程度上体现了焦点查找的一个原则,即控件在窗口上的位置是唯一查找依据,与控件在控件树中的层次无关。

  至于具体的查找算法,我们就不深入去研究了,有兴趣的同学可以自行阅读。

按键事件分发

  在“Android事件分发”分析了事件分发的分水岭:

final class ViewPostImeInputStage extends InputStage {
    public ViewPostImeInputStage(InputStage next) {
        super(next);
    }

    @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处理按键事件

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

    int groupNavigationDirection = 0;

    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) {
        if (mView.dispatchKeyShortcutEvent(event)) {
            return FINISH_HANDLED;
        }
        if (shouldDropInputEvent(q)) {
            return FINISH_NOT_HANDLED;
        }
    }

    // Apply the fallback event policy.
    if (mFallbackEventHandler.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (shouldDropInputEvent(q)) {
        return FINISH_NOT_HANDLED;
    }

    // Handle automatic focus changes.
    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的代码较长,不过我们的摸底是抽离核心步骤进行分析即可,processKeyEvent有两个核心内容:

  • 调用mView.dispatchKeyEvent(event),在控件树中进行按键事件分发;
  • 如果在上一步中没有View消耗事情,那么再调用performFocusNavigation判断该事件是否是方向键,控制焦点的移动;

事件分发

这里是调用ViewRootImpl内部字段mView.dispatchPointerEvent就行触摸事件分发,mView是什么呢?通过前面文章的分析我们能知道mView其实是PhoneWindow内部维护的DecorView,而DecorView继承FrameLayout,所以最终调用的是ViewGroup.dispatchKeyEvent

public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 1);
    }

    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        // 1.
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        // 2.
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }

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

标记1
如果此ViewGroup拥有焦点,则调用super.dispatchKeyEvent尝试消费事件;
标记2
如果此ViewGroup不拥有焦点,则将事件沿折mFocused链表进行传递;如果mFocused为ViewGroup,那么进入递归过程;

View.dispatchKeyEvent()

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;
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }

    if (event.dispatch(this, mAttachInfo != null
            ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }

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

首先由mOnKeyListener监听者尝试处理事件,如果mOnKeyListener返回true,那么整个案件事件结束。然后通过event.dispatch方法将事件发给View的指定回调,如onKeyDown()/onKeyUp()等。

焦点移动

performFocusNavigation方法:

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;
            }
        }
    }
    return false;
}
  1. 转换获取焦点移动的方向direction;
  2. mView.findFocus找到控件树中当前获得焦点的View;
  3. 以当前获得焦点控件focused为启动,调用focusSearch(direction)在direction方向上查找下一个获取焦点的控件;
  4. 调用requestFocus方法,实现旧焦点体系的移除和新焦点体系的建立(这点参考前面的分析);