Android焦点分发和移动的原理
如果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焦点常用的方法和原理,有建议或疑问可以交流一下。