2.2 findNextFocus
如果开发者没有指定nextFocusId,则用findNextFocus找指定方向上最近的视图
看一下这里的用法
focusables.clear();
// 2.2.1 找到所有isFocusable的View
root.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
// 2.2.2 从focusables中找到最近的一个
next = findNextFocus(root, focused, focusedRect, direction, focusables);
}
2.2.1 View.addFocusables,从root开始找所有isFocusable的视图
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
addFocusables(views, direction, FOCUSABLES_TOUCH_MODE);
}
public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
@FocusableMode int focusableMode) {
...
views.add(this);
}
如果root是一个单纯View,则添加自己,但这种情况很少见,大部分的root都是ViewGroup
// ViewGroup.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
...
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
child.addFocusables(views, direction, focusableMode);
}
}
}
if ((descendantFocusability != FOCUS_AFTER_DESCENDANTS
// No focusable descendants
|| (focusableCount == views.size())) &&
(isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())) {
super.addFocusables(views, direction, focusableMode);
}
}
对于ViewGroup来说,遍历并添加自己的所有isFocusable的child
这里有个descendantFocusability变量,有三个取值
- FOCUS_BEFORE_DESCENDANTS:在所有子视图之前获取焦点
- FOCUS_AFTER_DESCENDANTS: 在所有子视图之后获取焦点
- FOCUS_BLOCK_DESCENDANTS: 阻止所有子视图获取焦点,即使他们是focusable的
2.2.2 FocusFinder.findNextFocus
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// 2.2.2.1 取得考虑scroll之后的焦点Rect,该Rect是相对focused视图本身的
// fill in interesting rect from focused
focused.getFocusedRect(focusedRect);
// 2.2.2.2 将当前focused视图的坐标系,转换到root的坐标系中,统一坐标,以便进行下一步的计算
root.offsetDescendantRectToMyCoords(focused, focusedRect);
} else {
...
}
switch (direction) {
...
case View.FOCUS_UP:
case View.FOCUS_DOWN:
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
2.2.2.3 找出指定方向上的下一个focus视图
return findNextFocusInAbsoluteDirection(focusables, root, focused,
focusedRect, direction);
default:
throw new IllegalArgumentException("Unknown direction: " + direction);
}
}
2.2.2.1 focused.getFocusedRect(focusedRect);
public void getFocusedRect(Rect r) {
getDrawingRect(r);
}
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}
这里是取得考虑scroll之后的焦点Rect,该Rect是相对focused视图本身的
2.2.2.2 root.offsetDescendantRectToMyCoords(focused, focusedRect);
public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
offsetRectBetweenParentAndChild(descendant, rect, true, false);
}
/**
* Helper method that offsets a rect either from parent to descendant or
* descendant to parent.
*/
void offsetRectBetweenParentAndChild(View descendant, Rect rect,
boolean offsetFromChildToParent, boolean clipToBounds) {
// already in the same coord system :)
if (descendant == this) {
return;
}
ViewParent theParent = descendant.mParent;
// search and offset up to the parent
// 在View树上往上层层遍历,直到root为止
while ((theParent != null)
&& (theParent instanceof View)
&& (theParent != this)) {
if (offsetFromChildToParent) {
// 把focusedRect转换到当前当前parent的坐标系中去
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
...
} else {
...
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
// 继续往上找
descendant = (View) theParent;
theParent = descendant.mParent;
}
// now that we are up to this view, need to offset one more time
// to get into our coordinate space
if (theParent == this) {
if (offsetFromChildToParent) {
// 最后再转换一次,终于把focusedRect的坐标转换到了root的坐标中
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
} else {
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
} else {
throw new IllegalArgumentException("parameter must be a descendant of this view");
}
}
经过层层转换,最终把focused视图的坐标,转换到了root坐标系中。这样就统一了坐标,以便进行下一步的计算。
2.2.2.3 找出指定方向上的下一个focus视图
findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction);
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
// initialize the best candidate to something impossible
// (so the first plausible view will become the best choice)
mBestCandidateRect.set(focusedRect);
switch(direction) {
case View.FOCUS_LEFT:
// 先虚构出一个默认候选Rect,就是把focusedRect向右移一个"身位",按键向左,那么他肯定就是优先级最低的了
mBestCandidateRect.offset(focusedRect.width() + 1, 0);
break;
...
}
View closest = null;
int numFocusables = focusables.size();
// 遍历所有focusable的视图
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);
// only interested in other non-root views
if (focusable == focused || focusable == root) continue;
// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);
// 将focusable的坐标转换到root的坐标系中,统一坐标
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
// 进行比较,选出较好的那一个,如果都是默认候选的Rect差,则closest为null
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}
在统一坐标之后,对于所有focusable的视图,进行一次遍历比较,得到最“近”的视图作为下一个焦点视图。这里用到了一个方法isBetterCandidate,从两个候选Rect中找到在指定方向上离当前Rect最近的一个,具体算法这里不细讲了。
至此,就找到了下一个焦点视图,然后调用requestFocus方法,让其获得焦点。
小结
经过对源码的分析,系统本身寻找下一个焦点视图的过程是:
- 首先寻找用户指定了id的视图,从当前焦点视图的节点开始遍历,直到找到匹配该id的视图。也许存在多个相同id的视图,但是只会找到视图节点树中最近的一个。
- 如果没有指定id,则遍历找出所有isFocusable的视图,统一坐标系,然后计算出指定方向上离当前焦点视图最近的一个视图。
结合KeyEvent事件的流转,处理焦点的时机,按照优先级(顺序)依次是:
- dispatchKeyEvent
- mOnKeyListener.onKey回调
- onKeyDown/onKeyUp
- focusSearch
- 指定nextFocusId
- 系统自动从所有isFocusable的视图中找下一个焦点视图
以上任一处都可以指定焦点,一旦使用了就不再往下走。
很多视图控件就重写了其中一些方法。
比如ScrollView,它会在dispatchKeyEvent的时候,自己去处理,用来进行内部的焦点移动或者整体滑动。
// ScrollView.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
public boolean executeKeyEvent(KeyEvent event) {
mTempRect.setEmpty();
if (!canScroll()) {
if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
View currentFocused = findFocus();
if (currentFocused == this) currentFocused = null;
View nextFocused = FocusFinder.getInstance().findNextFocus(this,
currentFocused, View.FOCUS_DOWN);
// 如果不能滑动,则直接让下一个Focus视图获取焦点
return nextFocused != null
&& nextFocused != this
&& nextFocused.requestFocus(View.FOCUS_DOWN);
}
return false;
}
boolean handled = false;
// 如果可以滑动,则进行ScrollView本身的滑动
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
if (!event.isAltPressed()) {
handled = arrowScroll(View.FOCUS_UP);
} else {
handled = fullScroll(View.FOCUS_UP);
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (!event.isAltPressed()) {
handled = arrowScroll(View.FOCUS_DOWN);
} else {
handled = fullScroll(View.FOCUS_DOWN);
}
break;
case KeyEvent.KEYCODE_SPACE:
pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
break;
}
}
return handled;
}
由于在dispatchKeyEvent里优先处理的,因此对于滑动方向的KeyEvent,onKeyDown就监听不到了。这也就是为什么onKeyDown里居然截获不到按键事件的原因。
本文从源码的角度分析了焦点的移动原理,如果大家有兴趣可以一起多多交流。