本篇依然讲的是焦点方面的问题,还是老样子,先看下出问题的现象,gif走起~

从动图上可以看到,进入二级页面,焦点向下移动,编辑框没有获取到焦点,后向上移动焦点,才获取到,是不是很神奇?
我们知道EditText和Button一样,是默认可获取焦点的,但是这里面没有获取到,这里面页面不是listview,第一个item是封装了EditText的控件,下面的其他item是单独另一种风格封装的控件。
思路:
1、EditText被父View焦点拦截了?
2、焦点搜索规则导致向下移动,EditText不是最佳的可以获取焦点的view?
首先,迅速排除掉了第一个可能性,因为代码中没有设置拦截子view focus焦点。
那么重点看下第二个思路,依然从源码处入手,焦点搜索查找由
frameworks/base/core/java/android/view/FocusFinder.java负责。
//查找焦点,root是根view,指的是DecorView,focused指的是当前已经获取到焦点的view,direction是焦点移动方向
public final View findNextFocus(ViewGroup root, View focused, int direction) {
return findNextFocus(root, focused, null, direction);
}
1、查找
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
//我这里,focused为null
if (focused != null) {
next = findNextUserSpecifiedFocus(root, focused, direction);
}
if (next != null) {
return next;
}
//初始化承载可以获取焦点的view的list
ArrayList<View> focusables = mTempList;
try {
//查找前先清除list
focusables.clear();
//调用根view的获取可以focusable的view并且添加到list中去
root.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
//获取到焦点view的list,继续查找
next = findNextFocus(root, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
2、遍历查找根view下的所有可以获取焦点的view,并且放到list集合去。
ViewGroup.java addFocusables方法
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
//获取当前viewGroup焦点能力
final int descendantFocusability = getDescendantFocusability();
//如果不是阻塞子view获取焦点,那么开始遍历子view
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
if (shouldBlockFocusForTouchscreen()) {
focusableMode |= FOCUSABLES_TOUCH_MODE;
}
final int count = mChildrenCount;
final View[] children = mChildren;
//开始遍历
for (int i = 0; i < count; i++) {
final View child = children[i];
//如果子view可见。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
child.addFocusables(views, direction, focusableMode);
}
}
}
//子view添加完了,开始添加父view自己了。
if ((descendantFocusability != FOCUS_AFTER_DESCENDANTS
// No focusable descendants
|| (focusableCount == views.size())) &&
(isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())) {
super.addFocusables(views, direction, focusableMode);
}
}
好了,这里不继续往下贴源码了,结合项目bug来看,二级页面的EditText以及下面列表的item都是可以获取焦点的,所以都会被添加到这个list容器中,供FocusFinder用来在他们之间查找。
继续回到FocusFinder,看下其源码实现。
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
//如果当前已经存在获取焦点的view,设置当前焦点区域背景Rect区域
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// fill in interesting rect from focused
focused.getFocusedRect(focusedRect);
//将背景Rect转换为坐标系的值
root.offsetDescendantRectToMyCoords(focused, focusedRect);
} else {
if (focusedRect == null) {
focusedRect = mFocusedRect;
// make up a rect at top left or bottom right of root
switch (direction) {
case View.FOCUS_RIGHT:
case View.FOCUS_DOWN:
setFocusTopLeft(root, focusedRect);
break;
case View.FOCUS_FORWARD:
if (root.isLayoutRtl()) {
setFocusBottomRight(root, focusedRect);
} else {
setFocusTopLeft(root, focusedRect);
}
break;
case View.FOCUS_LEFT:
case View.FOCUS_UP:
setFocusBottomRight(root, focusedRect);
break;
case View.FOCUS_BACKWARD:
if (root.isLayoutRtl()) {
setFocusTopLeft(root, focusedRect);
} else {
setFocusBottomRight(root, focusedRect);
break;
}
}
}
}
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);
}
}
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
//设置了最合适的候选者背景位置
mBestCandidateRect.set(focusedRect);
//根据不同的焦点移动方向,调整候选区域
switch(direction) {
case View.FOCUS_LEFT:
mBestCandidateRect.offset(focusedRect.width() + 1, 0);
break;
case View.FOCUS_RIGHT:
mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
break;
case View.FOCUS_UP:
mBestCandidateRect.offset(0, focusedRect.height() + 1);
break;
case View.FOCUS_DOWN:
mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
}
//初始化最靠近的view
View closest = null;
int numFocusables = focusables.size();
//遍历查找焦点view list
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);
//如果是当前已经获取焦点的view或者是根view,跳过
if (focusable == focused || focusable == root) continue;
//获取候选者view的坐标值
focusable.getFocusedRect(mOtherRect);
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
//重点来了,选取最合适的下一个可以获取焦点的view
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}
下面是最最关键的部分了,重点来研究下,焦点匹配查找算法。
// 一共有三个Rect,source是当前可以获取焦点的区域,rect1是候选者区域,
rect2是选择项区域
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
// to be a better candidate, need to at least be a candidate in the first
// place :)
//如果候选view不满足条件,直接pass
if (!isCandidate(source, rect1, direction)) {
return false;
}
//如果候选view满足条件,继续看rect2,如果不满足条件,就选rect1
// we know that rect1 is a candidate.. if rect2 is not a candidate,
// rect1 is better
if (!isCandidate(source, rect2, direction)) {
return true;
}
// if rect1 is better by beam, it wins
//如果rect1,rect2都是候选者,都是合适的,继续在beamBeats维度上进行比较。
if (beamBeats(direction, source, rect1, rect2)) {
return true;
}
// if rect2 is better, then rect1 cant' be :)
//在beamBeats 比较中,rect2不满足,选rect1
if (beamBeats(direction, source, rect2, rect1)) {
return false;
}
//好了,如果rect1,rect2都满足,继续这个比较
// otherwise, do fudge-tastic comparison of the major and minor axis
return (getWeightedDistanceFor(
majorAxisDistance(direction, source, rect1),
minorAxisDistance(direction, source, rect1))
< getWeightedDistanceFor(
majorAxisDistance(direction, source, rect2),
minorAxisDistance(direction, source, rect2)));
}
这段代码,说实话,看起来还是有点费劲的,总结一下。
1、一个source区域,也就是被比较的位置范围,rect1,候选者view的位置范围,rect2 当前最合适的区域范围。
2、一共有三个比较维度,isCandidate,beamBeats,还有getWeightedDistanceFor比较,我们一个个看其实怎么比较的。
isCandidate 比较:
boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
switch (direction) {
case View.FOCUS_LEFT:
return (srcRect.right > destRect.right || srcRect.left >= destRect.right)
&& srcRect.left > destRect.left;
case View.FOCUS_RIGHT:
return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
&& srcRect.right < destRect.right;
case View.FOCUS_UP:
return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
&& srcRect.top > destRect.top;
case View.FOCUS_DOWN:
return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
&& srcRect.bottom < destRect.bottom;
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
这段代码什么意思呢?我画个示意图吧,方便你们理解。
以向下移动举例:

如果满足目标区域完全在源区域下方或者目标区域和源区域仅有图示的部分重叠,那么dest目标view是满足条件的。
beamBeats 主要是比较水平方向的区域(以上下焦点为例)
boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {
switch (direction) {
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
return (rect2.bottom >= rect1.top) && (rect2.top <= rect1.bottom);
case View.FOCUS_UP:
case View.FOCUS_DOWN:
return (rect2.right >= rect1.left) && (rect2.left <= rect1.right);
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
用图来表示:

getWeightedDistanceFor 比较维度。
这个维度在水平和垂直方向上做了一个权重比较。
return (getWeightedDistanceFor(
majorAxisDistance(direction, source, rect1),
minorAxisDistance(direction, source, rect1))
< getWeightedDistanceFor(
majorAxisDistance(direction, source, rect2),
minorAxisDistance(direction, source, rect2)));
int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
return 13 * majorAxisDistance * majorAxisDistance
+ minorAxisDistance * minorAxisDistance;
}
static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {
switch (direction) {
case View.FOCUS_LEFT:
return source.left - dest.left;
case View.FOCUS_RIGHT:
return dest.right - source.right;
case View.FOCUS_UP:
return source.top - dest.top;
case View.FOCUS_DOWN:
return dest.bottom - source.bottom;
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
static int minorAxisDistance(int direction, Rect source, Rect dest) {
switch (direction) {
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
// the distance between the center verticals
return Math.abs(
((source.top + source.height() / 2) -
((dest.top + dest.height() / 2))));
case View.FOCUS_UP:
case View.FOCUS_DOWN:
// the distance between the center horizontals
return Math.abs(
((source.left + source.width() / 2) -
((dest.left + dest.width() / 2))));
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
这又是一段很难理解的算法,说实话,我很难用文字描述出来,哪个比较的rect的13majorAxisDistancemajorAxisDistance+ minorAxisDistance * minorAxisDistance的数字越大,那么他越合适。以向下移动焦点为例,majorAxisDistance指的是dest的bottom-src的区域,也就是高度差,高度差和 minorAxisDistance水平差两个因素,高度差的权重与水平差的权重比13:1。
经过这段算法的仔细分析,造成我bug的问题就出现在最后一个比较维度,因为代码设置了 EditText的 gravity=right,导致Edittext的绘制区域背景靠右,在比较上面落后于“呼叫速率”那一个控件,找到原因后,就知道如何改了,将EditText gravity=left就可以了,或者让EditText的父view可以获取焦点,就可以了。
本文探讨了在Android应用中EditText无法获取焦点的问题,通过分析源码和故障现象,发现焦点搜索算法是关键。问题源于EditText的gravity设置,修复方法包括调整gravity为left或使父View可获取焦点。

1477

被折叠的 条评论
为什么被折叠?



