查找最近的可以获取焦点的控件的方法focusSearch(@FocusRealDirection int direction)
查找最近的可以获取焦点的控件是通过View类的focusSearch(@FocusRealDirection int direction)方法实现,代码如下:
/**
* Find the nearest view in the specified direction that can take focus.
* This does not actually give focus to that view.
*
* @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
*
* @return The nearest focusable in the specified direction, or null if none
* can be found.
*/
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
该方法通过获取焦点的控件调用,来寻找一个最接近的指定方向上的可以获取焦点的控件。父控件不为null的时候调用mParent.focusSearch(this, direction),大部分情况下,mParent 都是ViewGroup类型的,所以看下ViewGroup类型的focusSearch(View focused, int direction)方法:
@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;
}
如果当前控件是isRootNamespace(),则会调用FocusFinder类的findNextFocus()方法,如果不是则会通过mParent调用父控件的focusSearch()方法。isRootNamespace()就是检查PFLAG_IS_ROOT_NAMESPACE标识是否存在,一般就是DecorView类控件mDecor具有PFLAG_IS_ROOT_NAMESPACE标识。所以在这里就是通过mParent父控件链,一直到DecorView类控件mDecor,开始调用FocusFinder类的findNextFocus()方法,如下:
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;
}
该方法所做操作:
1、调用getEffectiveRoot(root, focused)得到实际的查找根控件
2、调用findNextUserSpecifiedFocus(effectiveRoot, focused, direction)得到用户特定指明的下一个控件,如果用户指明了,就返回该控件。
3、如果用户没有特定指明的下一个控件,需要根据一定的规则找到下一个可以获取焦点的控件。最后返回找到的控件作为结果。
该篇先说前两个步骤的相关代码。第3步看下一篇文章。
getEffectiveRoot(root, focused)
看一下getEffectiveRoot(root, focused)方法代码:
private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
if (focused == null || focused == root) {
return root;
}
ViewGroup effective = null;
ViewParent nextParent = focused.getParent();
do {
if (nextParent == root) {
return effective != null ? effective : root;
}
ViewGroup vg = (ViewGroup) nextParent;
if (vg.getTouchscreenBlocksFocus()
&& focused.getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TOUCHSCREEN)
&& vg.isKeyboardNavigationCluster()) {
// Don't stop and return here because the cluster could be nested and we only
// care about the top-most one.
effective = vg;
}
nextParent = nextParent.getParent();
} while (nextParent instanceof ViewGroup);
return root;
}
参数root就是DecorView类控件mDecor,该方法是检查如果目前获取焦点的控件处于键盘导航键区之内,那么实际查找的根控件可能是键区的根控件。键区的根控件还需要满足以下条件,容器控件应该忽略它本身及子控件的焦点请求和设备有一个触摸屏。如果满足这个条件,就将键区的根控件返回,并且键区是可能嵌套的,所以需要最外层的满足条件的键区根控件。如果没找到,就将参数root返回。
findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction)
再看第2步骤中的findNextUserSpecifiedFocus(effectiveRoot, focused, direction)方法:
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
// check for user specified next focus
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
View cycleCheck = userSetNextFocus;
boolean cycleStep = true; // we want the first toggle to yield false
while (userSetNextFocus != null) {
if (userSetNextFocus.isFocusable()
&& userSetNextFocus.getVisibility() == View.VISIBLE
&& (!userSetNextFocus.isInTouchMode()
|| userSetNextFocus.isFocusableInTouchMode())) {
return userSetNextFocus;
}
userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
if (cycleStep = !cycleStep) {
cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
if (cycleCheck == userSetNextFocus) {
// found a cycle, user-specified focus forms a loop and none of the views
// are currently focusable.
break;
}
}
}
return null;
}
先通过focused.findUserSetNextFocus(root, direction)获得用户指定的下一个方向的控件。但是该控件需要满足一定条件,才能作为结果返回。while循环里面可以看到具体条件,控件是可获得焦点的(isFocusable()),控件是可见的,控件不是在触摸模式(Android 控件获取焦点 文章中解释)或者在触摸模式下可以获得焦点。满足这几个条件,该控件就作为结果返回。如果不满足这几个结果,就查找找到的控件的用户指定的下一个方向的控件。再检查它是否满足以上条件,如果满足就作为结果返回。其中变量cycleStep的作用是为了避免用户指定的控件形成了一个循环,并且都不满足获取控件焦点的条件,如果发现循环了,会跳出循环,返回null。
再看下focused.findUserSetNextFocus(root, direction)方法看是怎么获得用户指定的下一个方向的控件,如下:
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 View rootView = root;
final View startView = this;
// Since we have forward links but no backward links, we need to find the view that
// forward links to this view. We can't just find the view with the specified ID
// because view IDs need not be unique throughout the tree.
return root.findViewByPredicateInsideOut(startView,
t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId)
== startView);
}
}
return null;
}
从这里可以看到参数direction的值可能为FOCUS_LEFT、FOCUS_RIGHT、FOCUS_UP、FOCUS_DOWN、FOCUS_FORWARD、FOCUS_BACKWARD,它们分别对应用户按导航键KeyEvent.KEYCODE_DPAD_LEFT、KeyEvent.KEYCODE_DPAD_RIGHT、KeyEvent.KEYCODE_DPAD_UP、KeyEvent.KEYCODE_DPAD_DOWN、KeyEvent.KEYCODE_TAB、KeyEvent.KEYCODE_TAB+( KEYCODE_SHIFT_LEFT或KEYCODE_SHIFT_RIGHT)触发的方向值。对应关系如下表,最后一列xml布局文件设置属性,是控件的成员变量mNextFocusXXId,在布局文件中对应属性。该属性是一个ID int值,后面就可以通过ID值找到对应的控件。
导航按键 | 寻找方向 | xml布局文件设置属性 |
KEYCODE_DPAD_LEFT | FOCUS_LEFT | nextFocusLeft |
KEYCODE_DPAD_RIGHT | FOCUS_RIGHT | nextFocusRight |
KEYCODE_DPAD_UP | FOCUS_UP | nextFocusUp |
KEYCODE_DPAD_DOWN | FOCUS_DOWN | nextFocusDown |
KEYCODE_TAB | FOCUS_FORWARD | nextFocusForward |
KEYCODE_TAB+( KEYCODE_SHIFT_LEFT或KEYCODE_SHIFT_RIGHT) | FOCUS_BACKWARD |
在该方法里,前五个方向导航的代码都是相似的,是先检查对应的属性id是否已经设置了,如果没设置,就返回null。如果设置了,就执行findViewInsideOutShouldExist()方法,通过设置的id值找到对应的控件。
在参数direction是FOCUS_BACKWARD时,是检查控件本身是否设置了ID值,如果没有设置,也返回null。如果设置了mID,则执行root.findViewByPredicateInsideOut()方法来查找。从列表看到,发生FOCUS_BACKWARD方法的查找的时候,是没有设置对应的属性的,这个是什么意思呢,这个是为了寻找设置了nextFocusForward的值等于当前焦点控件的控件,就是xml布局文件中nextFocusForward的id的值是当前焦点控件的控件。
findViewInsideOutShouldExist(View root, int id)
先看看findViewInsideOutShouldExist()方法:
private View findViewInsideOutShouldExist(View root, int id) {
return findViewInsideOutShouldExist(root, this, id);
}
private View findViewInsideOutShouldExist(View root, View start, int id) {
if (mMatchIdPredicate == null) {
mMatchIdPredicate = new MatchIdPredicate();
}
mMatchIdPredicate.mId = id;
View result = root.findViewByPredicateInsideOut(start, mMatchIdPredicate);
if (result == null) {
Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
}
return result;
}
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;
}
}
findViewInsideOutShouldExist()方法,会去调用root.findViewByPredicateInsideOut(start, mMatchIdPredicate)方法,这里的start就是咱们现在获取到的焦点的控件,是通过前面传递过来的。进入findViewByPredicateInsideOut()方法,会首先调用参数start的findViewByPredicateTraversal(predicate, childToSkip),start如果是View类型的,就会调用View类的findViewByPredicateTraversal(),代码如下:
protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
View childToSkip) {
if (predicate.test(this)) {
return (T) this;
}
return null;
}
可见满足predicate的test()方法,就可以,这个就是当前控件的id和predicate类的成员id值相等。如果相等,就返回该控件,不满足,就返回null。
如果返回不为null,或者start也就是当前调用findViewByPredicateInsideOut()方法的控件(代表已经沿着父控件链到达顶层祖先控件),就返回View。这俩条件都不满足,就会找到它的父控件,并且将当前控件设置为childToSkip,该变量代表着需要跳过的控件,因为已经必过了。父控件大多数都是ViewGroup类型的,看一下ViewGroup类的findViewByPredicateTraversal():
@Override
protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
View childToSkip) {
if (predicate.test(this)) {
return (T) this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewByPredicate(predicate);
if (v != null) {
return (T) v;
}
}
}
return null;
}
可见是先比较当前控件的id,如果满足就返回了。如果不满足,需要比较它的子控件的id,并且会跳过之前比较过的子控件。可见,root.findViewByPredicateInsideOut(start, mMatchIdPredicate)会把参数root包含的控件都进行比较。如果都没有被找到,会返回null。
root.findViewByPredicateInsideOut(View start, Predicate predicate)
该方法对应查找方向为FOCUS_BACKWARD的执行方法,
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;
}
}
可见它和上面findViewInsideOutShouldExist(View root, int id)代码是相似的,不过就是判断条件predicate是不同的,FOCUS_BACKWARD的判断条件是t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId) == startView,这个就是寻找root中的控件的mNextFocusForwardId的值等于当前获取焦点的ID的控件。
这样findNextFocus()的第2步的findNextUserSpecifiedFocus(effectiveRoot, focused, direction)方法就完毕了。