如果Activity里有EditText,那么打开Activity后,EditText会自动获取焦点。
为什么呢,很多时候我们不想要这个效果,参照网上的方法将father layout设置成获取焦点就解决问题。知其然知其所以然,翻了一下代码,答案隐藏在ViewRootImpl.performTraversals方法中,就是那个view绘制的核心方法,中间有一段:
private void performTraversals() { //... if (mFirst) { // handle first focus request if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()=" + mView.hasFocus()); if (mView != null) { if (!mView.hasFocus()) { mView.requestFocus(View.FOCUS_FORWARD); if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view=" + mView.findFocus()); } else { if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view=" + mView.findFocus()); } } } //... }
当是第一个view时,会调用requestFocus获取焦点。ViewRootImpl相关内容自行看android的窗口机制,这个不是今日的目标,本文要讲的是:
- requestFocus和背后的焦点分发机制;
- clearFocus真的无效吗?
- 如果让焦点按意志移动。

demo
写了个测试用的demo,上面很多EditText啦,还有上下左右前后等焦点的控制键。
View是否能获取焦点
让View获取焦点,直接调用requestFocus,最终会调用到requestFocusNoSearch:
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { return false; } // need to be focusable in touch mode if in touch mode 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; } handleFocusGainInternal(direction, previouslyFocusedRect); return true; }
requestFocusNoSearch校验View的属性,获取焦点的前提条件是“可见的”和“可聚焦的”,并且“可聚焦的”需要同时符合:
android:focusable="true" android:focusableInTouchMode="true"
接着调用了hasAncestorThatBlocksDescendantFocus,这个需要了解View的descendantFocusability属性。这对我来说是新概念,以前没有用过,后文还会涉及,现在先储备知识。
- beforeDescendants:ViewGroup会优先其子view而获取到焦点
- afterDescendants:ViewGroup只有当其子view不需要获取焦点时才获取焦点
- blocksDescendants:ViewGroup会覆盖子view而直接获得焦点
作者:展翅而飞
private boolean hasAncestorThatBlocksDescendantFocus() { final boolean focusableInTouchMode = isFocusableInTouchMode(); ViewParent ancestor = mParent; while (ancestor instanceof ViewGroup) { final ViewGroup vgAncestor = (ViewGroup) ancestor; if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) { return true; } else { ancestor = vgAncestor.getParent(); } } return false; }
hasAncestorThatBlocksDescendantFocus就很好理解,如果有祖先ViewGroup设置成blocksDescendants,那么它的子孙View都不能获取焦点。
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " requestFocus()"); } if ((mPrivateFlags & PFLAG_FOCUSED) == 0) { mPrivateFlags |= PFLAG_FOCUSED; View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null; if (mParent != null) { mParent.requestChildFocus(this, this); } if (mAttachInfo != null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); } }
handleFocusGainInternal实现View获取焦点的具体逻辑,所以requestFocusNoSearch默认返回true。handleFocusGainInternal里面最重要的是调用了mParent.requestChildFocus,通知它的父view处理焦点。mParent的类型是ViewParent,每一个view都会保存它的父view,基本上实现类就是ViewGroup。
然后触发onFocusChanged这个listener,最后触发invalidate进行ui更新。
在继续探究requestChildFocus的代码前,先认真讲讲焦点的分发过程。
焦点分发过程
有个大家族,已经经历多代,族人角色可以这样定义:
- 成员:View
- 有子女的成员:ViewGroup
- 辈分最高的长老:DecorView
家族中有一件宝贝,持有在一名成员手上。别的家族想参观,首先需要找长老。
长老不会一个个成员问,而是先找大儿子问,再找二儿子问,如此类推。儿子们也是这样问自己的儿子,过程也是如此类推。一层层地问,直到最后找到宝贝的持有人,再一层层向上通知。
宝贝就是焦点,寻找宝贝的过程就是焦点分发的过程。
ViewGroup对焦点的处理
看回handleFocusGainInternal里的requestChildFocus,view如果需要获取焦点,需要通知它的父view处理,所以我们来看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 super.unFocus(focused); // We had a previous notion of who had focus. Clear it. if (mFocused != child) { if (mFocused != null) { mFocused.unFocus(focused); } mFocused = child; } if (mParent != null) { mParent.requestChildFocus(this, focused); } }
首先会调用unFocus清除自己的焦点,mFocused表示ViewGroup内部是否持有焦点,如果mFocused不是目标获取焦点的child,那么再清除当前mFocused的焦点,并将child赋给mFocused。
最后继续通过mParent递归调用requestChildFocus,直到顶层view,保证焦点唯一。
ViewGroup也可以获取焦点,和上面View的requestFocus方法不同:
@Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " ViewGroup.requestFocus direction=" + direction); } 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属性的铺垫,ViewGroup的requestFocus很容易理解。block状态时,焦点查找交还给父View;before状态时,优先自己获取焦点;after状态时,优先子view获取焦点。
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方法就是向子view询问焦点的逻辑,区分正反两种查找方向。只要有一个view成功获取到焦点,就返回true。
清除焦点
上面没有讲view失去焦点的处理,现在来看下ViewGroup的unFocus,还要探究一下clearFocus“无效”的背后原理。
@Override 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,最终调用了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); } } }
clearFocusInternal是真正操作焦点失去的地方,通过mParent调用ViewGroup的clearChildFocus。
@Override public void clearChildFocus(View child) { if (DBG) { System.out.println(this + " clearChildFocus()"); } mFocused = null; if (mParent != null) { mParent.clearChildFocus(this); } }
clearChildFocus将当前mFocused置空,通过递归向上处理直到顶层view,保证整颗view树失去焦点。
注意,unFocus我们并不能调用,View提供clearFocus,内部同样调用clearFocusInternal,它们不同的地方是refocus传入不同。
作者:展翅而飞
boolean rootViewRequestFocus() { final View root = getRootView(); return root != null && root.requestFocus(); }
refocus的不同,决定是否会触发rootViewRequestFocus,因此clearFocus“无效”的问题很好理解。如果一个页面只有一个EditText,使用clearFocus清除焦点,马上地,焦点又被设置上啦,所以会有清除无效的错觉。因此,让父view自动获取焦点是很好的解决方法。
焦点查找
@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和ViewGroup提供了focusSearch方法进行焦点查找,入参是当前获取焦点的view和目标查找方向,返回下一个应该获取焦点的view。focusSearch调用的是FocusFinder类,直接来看FocusFinder最常用的findNextFocus:
作者:展翅而飞
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { //1 View next = null; if (focused != null) { next = findNextUserSpecifiedFocus(root, focused, direction); } if (next != null) { return next; } //2 ArrayList<View> focusables = mTempList; try { focusables.clear(); root.addFocusables(focusables, direction); if (!focusables.isEmpty()) { next = findNextFocus(root, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }
1、预设焦点
看标记1,调用了findNextUserSpecifiedFocus,查找用户预设不同方向获取焦点的View。
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { // check for user specified next focus View userSetNextFocus = focused.findUserSetNextFocus(root, direction); if (userSetNextFocus != null && userSetNextFocus.isFocusable() && (!userSetNextFocus.isInTouchMode() || userSetNextFocus.isFocusableInTouchMode())) { return userSetNextFocus; } return null; }
里面调用了View.findUserSetNextFocus,在xml文件中,我们可以使用android:nextFocusLeft、android:nextFocusRight、android:nextFocusUp、android:nextFocusDown、android:nextFocusForward指定对应的View。
2、自动查找焦点
如果没有预设,就由程序自动查找。标记2收集root下所有能获取焦点的view,调用重载版本的findNextFocus方法。
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables) { //1 //... //2 switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, direction); case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); } }
这里我省略了标记1一大段代码,大约逻辑是计算焦点的矩形范围,如果当前已经有view得到焦点,直接通过view计算即可;如果没有,那么通过root和方向计算,比较简单,就不贴出来占地方。
标记2根据查找方向使用不同算法,前项和后项使用findNextFocusInRelativeDirection,上下左右使用findNextFocusInAbsoluteDirection。
对于前项和后项这种按序的查找,很容易想到需要对view进行排序,这里使用了内部类SequentialFocusComparator,根据view矩形的高低左右比较。
对于上下左右方向,需要在能获取焦点view中比较出最适合的一个。首先会设置一个差的结果,然后对每一个可以获取焦点的view调用isBetterCandidate,找到方向上离自己最近最合适的一个。算法比较复杂,有兴趣自行研究。
private fun doFocusUp() { currentFocus?.let { currentFocus.focusSearch(View.FOCUS_UP)?.requestFocus() } } private fun doFocusDown() { currentFocus?.let { currentFocus.focusSearch(View.FOCUS_DOWN)?.requestFocus() } } private fun doFocusLeft() { currentFocus?.let { currentFocus.focusSearch(View.FOCUS_LEFT)?.requestFocus() } } private fun doFocusRight() { currentFocus?.let { currentFocus.focusSearch(View.FOCUS_RIGHT)?.requestFocus() } } private fun doFocusForward() { val focusView = currentFocus ?: return FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_BACKWARD)?.requestFocus() } private fun doFocusNext() { val focusView = currentFocus ?: return FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_FORWARD)?.requestFocus() }
demo里上下左右前后六个方向就是使用FocusFinder实现。focusSearch限制了只能使用上下左右四个方向,前后两个方向直接调用FocusFinder。
小结
本文总结了android焦点常用的方法和原理,有建议或疑问可以交流一下。