查找最近的可以获取焦点的控件是通过View类的focusSearch(@FocusRealDirection int direction)方法实现,代码如下:
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; } ArrayListfocusables = 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)方法代码:
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值找到对应的控件。
在该方法里,前五个方向导航的代码都是相似的,是先检查对应的属性id是否已经设置了,如果没设置,就返回null。如果设置了,就执行findViewInsideOutShouldExist()方法,通过设置的id值找到对应的控件。
在参数direction是FOCUS_BACKWARD时,是检查控件本身是否设置了ID值,如果没有设置,也返回null。如果设置了mID,则执行root.findViewByPredicateInsideOut()方法来查找。从列表看到,发生FOCUS_BACKWARD方法的查找的时候,是没有设置对应的属性的,这个是什么意思呢,这个是为了寻找设置了nextFocusForward的值等于当前焦点控件的控件,就是xml布局文件中nextFocusForward的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 finalT findViewByPredicateInsideOut( View start, Predicate 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(),代码如下:
protectedT findViewByPredicateTraversal(Predicate 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 protectedT findViewByPredicateTraversal(Predicate 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 finalT findViewByPredicateInsideOut( View start, Predicate 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)方法就完毕了。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)