该篇文章承接Android 查找最近的可以获取焦点的控件(一)
findNextFocus()第3步
将其相关代码摘录如下:
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
先将effectiveRoot中可以获取焦点的控件都添加到focusables队列中,然后focusables不空的时候,执行重载方法findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables),根据导航方向direction来获得下一个控件。
effectiveRoot.addFocusables(focusables, direction)
先看看将可获取焦点的控件添加到focusables:
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
}
先判断是不是处在触摸模式下,如果是参数是FOCUSABLES_TOUCH_MODE,如果不是触摸模式参数为FOCUSABLES_ALL,接着调用另一个重载函数。这个重载函数根据调用者是View类型还是ViewGroup类型来调用不同的实现,先看看View类的:
public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
@FocusableMode int focusableMode) {
if (views == null) {
return;
}
if (!canTakeFocus()) {
return;
}
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
&& !isFocusableInTouchMode()) {
return;
}
views.add(this);
}
该方法先检查参数views不能为null,然后检查控件是可以获取焦点的,如果有一个不能就直接返回了。再接着检查参数focusableMode是否设置了FOCUSABLES_TOUCH_MODE标识,如果设置了,代表是在触摸模式下,如果在这种情况下,控件不是isFocusableInTouchMode()的,也就直接返回了,不能添加到参数views中。如果上面检查通过,则将本控件添加到views中。
再看看ViewGroup类的addFocusables(ArrayList views, int direction, int focusableMode):
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
if (focusSelf) {
super.addFocusables(views, direction, focusableMode);
}
return;
}
if (blockFocusForTouchscreen) {
focusableMode |= FOCUSABLES_TOUCH_MODE;
}
if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
super.addFocusables(views, direction, focusableMode);
}
int count = 0;
final View[] children = new View[mChildrenCount];
for (int i = 0; i < mChildrenCount; ++i) {
View child = mChildren[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
children[count++] = child;
}
}
FocusFinder.sort(children, 0, count, this, isLayoutRtl());
for (int i = 0; i < count; ++i) {
children[i].addFocusables(views, direction, focusableMode);
}
// When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
// there aren't any focusable descendants. this is
// to avoid the focus search finding layouts when a more precise search
// among the focusable children would be more interesting.
if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
&& focusableCount == views.size()) {
super.addFocusables(views, direction, focusableMode);
}
}
该方法主要做了以下事情:
1、记录一些状态,参数views的大小,当前容器与子类获取焦点关系(descendantFocusability),触摸模式下是否应该阻阻止获取焦点(blockFocusForTouchscreen),容器本身能否获取焦点(focusSelf)。
2、在容器阻止子控件获取焦点的情况下,并且容器本身可以获取焦点,这个时候调用父类View的addFocusables(views, direction, focusableMode),检查是否能将本身加入views,并且在这种情况下,就退出不再向下执行。
3、如果触摸模式下是否应该阻阻止获取焦点,将参数focusableMode的加上FOCUSABLES_TOUCH_MODE标识。
4、如果它被设置成在子孙控件之前获取焦点,并且容器本身可以获取焦点,这个时候调用父类View的addFocusables(views, direction, focusableMode)。
5、找到容器的可见的子控件,放到数组children中,接着再对它进行排序(调用FocusFinder.sort())。
6、再对每个排序的子控件递归调用addFocusables(views, direction, focusableMode)方法。
7、如果容器设置了在子孙控件之后获取焦点,并且本身可以获取焦点,并且前面执行过添加控件之后的数量和1步骤中记录的数量一样的情况下(其实就是容器没有可以获取焦点的子孙控件),再调用父类View的addFocusables(views, direction, focusableMode)。
从这个方法中可以知道FOCUS_BLOCK_DESCENDANTS、FOCUS_BEFORE_DESCENDANTS、FOCUS_AFTER_DESCENDANTS的是如何使用的。
shouldBlockFocusForTouchscreen()方法在Android 控件获取焦点有讲到,super.addFocusables(views, direction, focusableMode)刚才也有讲到,下面主要说一下对子控件的排序,就是FocusFinder.sort(children, 0, count, this, isLayoutRtl()):
public static void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
getInstance().mFocusSorter.sort(views, start, end, root, isRtl);
}
public void sort(View[] views, int start, int end, ViewGroup root, boolean isRtl) {
int count = end - start;
if (count < 2) {
return;
}
if (mRectByView == null) {
mRectByView = new HashMap<>();
}
mRtlMult = isRtl ? -1 : 1;
for (int i = mRectPool.size(); i < count; ++i) {
mRectPool.add(new Rect());
}
for (int i = start; i < end; ++i) {
Rect next = mRectPool.get(mLastPoolRect++);
views[i].getDrawingRect(next);
root.offsetDescendantRectToMyCoords(views[i], next);
mRectByView.put(views[i], next);
}
// Sort top-to-bottom
Arrays.sort(views, start, count, mTopsComparator);
// Sweep top-to-bottom to identify rows
int sweepBottom = mRectByView.get(views[start]).bottom;
int rowStart = start;
int sweepIdx = start + 1;
for (; sweepIdx < end; ++sweepIdx) {
Rect currRect = mRectByView.get(views[sweepIdx]);
if (currRect.top >= sweepBottom) {
// Next view is on a new row, sort the row we've just finished left-to-right.
if ((sweepIdx - rowStart) > 1) {
Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
}
sweepBottom = currRect.bottom;
rowStart = sweepIdx;
} else {
// Next view vertically overlaps, we need to extend our "row height"
sweepBottom = Math.max(sweepBottom, currRect.bottom);
}
}
// Sort whatever's left (final row) left-to-right
if ((sweepIdx - rowStart) > 1) {
Arrays.sort(views, rowStart, sweepIdx, mSidesComparator);
}
mLastPoolRect = 0;
mRectByView.clear();
}
通过views[i].getDrawingRect(next),将控件的可见边框上下左右位置放入一个Rect类型的对象中,然后再调用root.offsetDescendantRectToMyCoords(views[i], next)将子控件的可见边框位置坐标转化成root容器的坐标位置。看下View类的getDrawingRect(Rect outRect):
/**
* Return the visible drawing bounds of your view. Fills in the output
* rectangle with the values from getScrollX(), getScrollY(),
* getWidth(), and getHeight(). These bounds do not account for any
* transformation properties currently set on the view, such as
* {@link #setScaleX(float)} or {@link #setRotation(float)}.
*
* @param outRect The (scrolled) drawing bounds of the view.
*/
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}
可见边框的位置坐标是分别需要加上mScrollX和mScrollY的,mScrollX和mScrollY是相当于把控件的坐标轴进行偏移。并且通过注释知道,边框不会起变化在变换属性时,像设置缩放或旋转。
再看向父控件进行坐标转化的代码offsetDescendantRectToMyCoords(View descendant, Rect rect):
public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
offsetRectBetweenParentAndChild(descendant, rect, true, false);
}
void offsetRectBetweenParentAndChild(View descendant, Rect rect,
boolean offsetFromChildToParent, boolean clipToBounds) {
// already in the same coord system :)
if (descendant == this) {
return;
}
ViewParent theParent = descendant.mParent;
// search and offset up to the parent
while ((theParent != null)
&& (theParent instanceof View)
&& (theParent != this)) {
if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
if (clipToBounds) {
View p = (View) theParent;
boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
p.mBottom - p.mTop);
if (!intersected) {
rect.setEmpty();
}
}
} else {
if (clipToBounds) {
View p = (View) theParent;
boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
p.mBottom - p.mTop);
if (!intersected) {
rect.setEmpty();
}
}
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
descendant = (View) theParent;
theParent = descendant.mParent;
}
// now that we are up to this view, need to offset one more time
// to get into our coordinate space
if (theParent == this) {
if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
} else {
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
} else {
throw new IllegalArgumentException("parameter must be a descendant of this view");
}
}
可见是调用rect.offset(descendant.mLeft - descendant.mScrollX, descendant.mTop - descendant.mScrollY)来得到在父控件中的坐标(这个坐标值是已经加上父控件的mScrollX或mScrollY),所以再到父控件的父控件时,再减去父控件的mScrollX或mScrollY。这样循环到顶,就得到了在焦点控件的边框在顶控件中的坐标位置。
mTopsComparator是一个Comparator,用来排序。它是根据控件的top大小升序排列,如果相等,再按照bottom大小升序排列。mSidesComparator也是一个Comparator。它根据布局是左到右,还是右到左,来定升序还是降序排列。如果布局是左到右,那么首先按照控件的left大小升序排列,如果相等,再按照right大小升序排列。如果布局是右到左,就是降序。
看代码里面,首先按照mTopsComparator进行了一次排序。这个时候,views里面是按照top大小升序排列,如果相等,再按照bottom大小升序排列。然后,又使用了一次循环,进行mSidesComparator排序。这块代码是什么意思呢?这个是为了给上下有重叠的控件进行mSidesComparator排序。按照此时views中的顺序,找到第一个与之前控件上下不相交叉的控件(就是条件currRect.top >= sweepBottom)。找到的这个控件之前的控件数量是不是超过1个,超过1个((sweepIdx - rowStart) > 1),就需要对这几个控件进行mSidesComparator排序。然后再接着寻找第二个与之前控件上下不相交叉的控件,如果第二个和第一个之间(不包括第二个控件)的控价的数量超过1个,也进行mSidesComparator排序。一直到循环结束。如果(sweepIdx - rowStart) > 1,说明最后还有几个上下相交的控件,也进行mSidesComparator排序。
所以最后排序完成之后,结果就是按照上下不相交,分成几段。每段之间的成员再按照mSidesComparator排序。
这样effectiveRoot.addFocusables(focusables, direction)里的方法就分析完毕。
以下几点可知:
1、在容器设置了FOCUS_BLOCK_DESCENDANTS标识的情况下,如果容器可以获取焦点,就只会添加容器本身;如果容器不可以获取焦点,什么也不会添加。
2、在容器设置了FOCUS_BEFORE_DESCENDANTS标识的情况下,如果容器可以获取焦点,则容器是比它的子控件先加入可获取焦点控件集合中的。
3、子控件添加的顺序是按照上下不相交,分成几段。每段之间的成员再按照mSidesComparator排序。
4、在容器设置了FOCUS_AFTER_DESCENDANTS标识的情况下,只有在子控件都不可以获取焦点的时候,才可能添加容器控件。
再看第3步的重载方法findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables)
findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables)
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// fill in interesting rect from focused
focused.getFocusedRect(focusedRect);
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);
}
}
该方法的代码分为两部分,上面的if else是为了给focusedRect赋值,下面的switch case是为了执行查找对应方向上的控件。
focusedRect赋值部分
先看看为focusedRect赋值部分,参数focused 是获取焦点的控件。如果它不为空,则将focused 的边框位置转化成root控件的坐标值。
如果focused为空,则根据方向的值来设置focusedRect。看一下setFocusBottomRight(ViewGroup root, Rect focusedRect),setFocusTopLeft(ViewGroup root, Rect focusedRect):
private void setFocusBottomRight(ViewGroup root, Rect focusedRect) {
final int rootBottom = root.getScrollY() + root.getHeight();
final int rootRight = root.getScrollX() + root.getWidth();
focusedRect.set(rootRight, rootBottom, rootRight, rootBottom);
}
private void setFocusTopLeft(ViewGroup root, Rect focusedRect) {
final int rootTop = root.getScrollY();
final int rootLeft = root.getScrollX();
focusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
}
setFocusBottomRight(ViewGroup root, Rect focusedRect)是设置focusedRect为root控件的边框左上角,setFocusBottomRight()是设置ocusedRect为root控件的边框右下角。这样就能根据代码,得到各个方向设置的区域不同。
查找对应方向上的控件
可见根据方向的不同,调用了两个方法。先看看设置了FOCUS_UP、FOCUS_DOWN、FOCUS_LEFT、FOCUS_RIGHT调用的方法findNextFocusInAbsoluteDirection(focusables, root, focused,focusedRect, direction):
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
// initialize the best candidate to something impossible
// (so the first plausible view will become the best choice)
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 closest = null;
int numFocusables = focusables.size();
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);
// only interested in other non-root views
if (focusable == focused || focusable == root) continue;
// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}
先设置一个最好的矩形mBestCandidateRect,不过这个最好的矩阵最开始设置成了一个不太可能的位置,这样后面会通过比较就能将这个去掉。接着就通过一个循环将focusables中所有能获取焦点的控件的边框位置,与最好的位置mBestCandidateRect进行比较,循环比较出的最好的位置的一个控件就是所要找到的控件。
看看刚开始是怎么将mBestCandidateRect设置成一个不可能的位置。先将mBestCandidateRect设置成了focusedRect,focusedRect是获取焦点控件的边框的位置(focused不为null的情况下)。在方向是FOCUS_LEFT,mBestCandidateRect.offset(focusedRect.width() + 1, 0),本来是找左面的控件,结果将mBestCandidateRect向右偏移了一个控件的宽度+1的距离。所以要找的控件可能不可能在这面。同样其他方向查找的时候,也是这样。
接下来再看看怎么进行比较的,主要调用了isBetterCandidate(int direction, Rect source, Rect rect1, Rect 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 :)
if (!isCandidate(source, rect1, direction)) {
return false;
}
// 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
if (beamBeats(direction, source, rect1, rect2)) {
return true;
}
// if rect2 is better, then rect1 cant' be :)
if (beamBeats(direction, source, rect2, rect1)) {
return false;
}
// 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)));
}
该方法主要调用了isCandidate(),beamBeats()方法,先看看这两个方法:
isCandidate(Rect srcRect, Rect destRect, int direction):
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}.");
}
isCandidate(Rect srcRect, Rect destRect, int direction)主要根据不同查找方向,来做比较。像FOCUS_LEFT方向的时候,destRect的左边界一定得在srcRect的左边界的左面,destRect的右边界一定得在srcRect的右边界的左面。但是destRect的右边界与srcRect的左边界可以相重叠。其他方向同理,自行分析。
再看看beamBeats(int direction, Rect source, Rect rect1, Rect rect2):
boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);
// if rect1 isn't exclusively in the src beam, it doesn't win
if (rect2InSrcBeam || !rect1InSrcBeam) {
return false;
}
// we know rect1 is in the beam, and rect2 is not
// if rect1 is to the direction of, and rect2 is not, rect1 wins.
// for example, for direction left, if rect1 is to the left of the source
// and rect2 is below, then we always prefer the in beam rect1, since rect2
// could be reached by going down.
if (!isToDirectionOf(direction, source, rect2)) {
return true;
}
// for horizontal directions, being exclusively in beam always wins
if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
return true;
}
// for vertical directions, beams only beat up to a point:
// now, as long as rect2 isn't completely closer, rect1 wins
// e.g for direction down, completely closer means for rect2's top
// edge to be closer to the source's top edge than rect1's bottom edge.
return (majorAxisDistance(direction, source, rect1)
< majorAxisDistanceToFarEdge(direction, source, rect2));
}
beamsOverlap()方法是根据查找方向用来判断另外的方向上是否有交叉。
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}.");
}
如果查找方向是FOCUS_LEFT或FOCUS_RIGHT,就看竖直方向是否有交叉;如果方向是FOCUS_UP或FOCUS_DOWN,就看水平方向是否有交叉。
beamBeats()如果(rect2InSrcBeam || !rect1InSrcBeam),说明rect2与焦点控件有交叉或者rect1没有交叉,返回false,还要进行后续比较。isToDirectionOf(direction, source, rect2)是用来根据查找方向,判断rect2是不是完全位于source的查找方向那一面。例如查找方向FOCUS_LEFT,就是判断rect2是不是完全位于source的左面。取反就是判断rect2不是完全位于source的左面。如果不是就返回true。代表rect1比rect2更适合。
beamBeats()接着向下判断,如果查找方向是FOCUS_LEFT或者FOCUS_RIGHT,也返回true,标识rect1比rect2更优。如果上面的都没有满足,并且方向是FOCUS_UP或者FOCUS_DOWN,会接着进行判断。看一下majorAxisDistance(direction, source, rect1):
static int majorAxisDistance(int direction, Rect source, Rect dest) {
return Math.max(0, majorAxisDistanceRaw(direction, source, dest));
}
static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
switch (direction) {
case View.FOCUS_LEFT:
return source.left - dest.right;
case View.FOCUS_RIGHT:
return dest.left - source.right;
case View.FOCUS_UP:
return source.top - dest.bottom;
case View.FOCUS_DOWN:
return dest.top - source.bottom;
}
throw new IllegalArgumentException("direction must be one of "
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
}
主要关注方向是FOCUS_UP或者FOCUS_DOWN,如果是FOCUS_UP,结果则为source的顶边到rect1的底边的距离,如果小于0,取0值。如果是FOCUS_DOWN,结果则为rect1的顶边到source的底边的距离,如果小于0,取0值。
看一下majorAxisDistanceToFarEdge(direction, source, rect2):
static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {
return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));
}
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}.");
}
主要关注方向是FOCUS_UP或者FOCUS_DOWN,如果是FOCUS_UP,结果则为source的顶边到rect2的顶边的距离,如果小于1,取1值。如果是FOCUS_DOWN,结果则为rect2的底边到source的底边的距离,如果小于1,取1值。
比较一下,如果FOCUS_DOWN,则是rect1的顶边到source的底边的距离(如果大于0)小于rect2的底边到source的底边的距离(如果大于1),则返回true,如果不是返回false。
比较一下,如果FOCUS_UP,则是source的顶边到rect1的底边的距离(如果大于0)小于source的顶边到rect2的顶边的距离(如果大于1),则返回true,如果不是返回false。
现在可以分析isBetterCandidate()的逻辑了。
isBetterCandidate()调用了2次isCandidate(),不过传递的参数不同,第一次传递的是待比较的控件的位置,这样如果第一次比较isCandidate()都不满足,说明待比较的控件不满足条件(像FOCUS_LEFT,它不在目前获取焦点的左面(可以交叉)),这个时候,不用向后比较了,直接就返回false了。如果它满足了条件,会进行第二次isCandidate(),这次传入的参数是之前最好的位置,如果第二次返回false,说明它不满足,而待比较的控件满足,这个时候,就找到了比较好的选择,返回true。如果两个都满足条件,会继续进行下面的比较。
isBetterCandidate()调用了2次beamBeats(),不过传递的后两个参数不同,这里rect1代表待比较的控件,rect2代表之前比较找到的最优的控件。第一次是rect1在前,rect2在后,这次调用beamBeats(direction, source, rect1, rect2)如果返回true,代表rect1优于rect2,直接返回了true。第二次调用rect2在前,rect1在后,如果这次调用beamBeats(direction, source, rect2, rect1)返回true,代表rect2优于rect1,所以返回false。
isBetterCandidate()如果到现在还没比较出来结果,就会进行最后的比较了。
看一下这个算法getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance):
/**
* Fudge-factor opportunity: how to calculate distance given major and minor
* axis distances. Warning: this fudge factor is finely tuned, be sure to
* run all focus tests if you dare tweak it.
*/
long getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance) {
return 13 * majorAxisDistance * majorAxisDistance
+ minorAxisDistance * minorAxisDistance;
}
这个方法给出的主轴距离(参数majorAxisDistance)与次轴距离(minorAxisDistance)的计算距离的一种方式。这个计算公式就是代码中的,其中13可能就是注释中说的模糊因数,并且注释中还提到了,这个模糊因数已经调整好了,如果自己再敢调整的话,确保做好测试。
再看看主轴距离和次轴距离的计算方式:
rect1到source的主轴距离是majorAxisDistance(direction, source, rect1),这个刚才上面说过了,计算的就是根据对应方向,在对应的方向轴上的不同边框的距离。例如,方向是FOCUS_LEFT,则是水平方向,计算距离的时候,使用source的左边框到rect1的右边框的距离。
rect1到source的次轴距离是minorAxisDistance(direction, source, rect1)),
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}.");
}
次轴是与主轴相对着的,如果主轴是水平方向,次轴就是上下方向。如果主轴是上下方向,次轴就是水平方向。次轴是上下方向的计算方式,是source的中心点到rect1的中心点的竖直距离的绝对值。次轴是水平方向的计算方式,是source的中心点到rect1的中心点的水平距离的绝对值。
rect2到source的主轴距离与次轴距离的计算方式同上面分析。
分析完上面的,就能计算isBetterCandidate的最后一步的计算方式了,调用getWeightedDistanceFor(long majorAxisDistance, long minorAxisDistance)得到待比较控件的相关值与之前最适合控件的相关值,如果前者小于后者,则认为前者比后者更优;否则,认为后者更优。
查找相对方向上的控件
这个是查找方向为FOCUS_FORWARD或者FOCUS_BACKWARD时候调用findNextFocusInRelativeDirection(ArrayList focusables, ViewGroup root, View focused, Rect focusedRect, int direction)方法:
private View findNextFocusInRelativeDirection(ArrayList<View> focusables, ViewGroup root,
View focused, Rect focusedRect, int direction) {
try {
// Note: This sort is stable.
mUserSpecifiedFocusComparator.setFocusables(focusables, root);
Collections.sort(focusables, mUserSpecifiedFocusComparator);
} finally {
mUserSpecifiedFocusComparator.recycle();
}
final int count = focusables.size();
switch (direction) {
case View.FOCUS_FORWARD:
return getNextFocusable(focused, focusables, count);
case View.FOCUS_BACKWARD:
return getPreviousFocusable(focused, focusables, count);
}
return focusables.get(count - 1);
}
可见首先对focusables集合进行排序,然后根据对应的方向是FOCUS_FORWARD,调用getNextFocusable(focused, focusables, count);如果对应的方向是FOCUS_BACKWARD,调用getPreviousFocusable(focused, focusables, count)方法。
首先看下对focusables集合进行排序,排序使用的是mUserSpecifiedFocusComparator,它是UserSpecifiedFocusComparator对象。看下它的排序实现,先看setFocusables(List focusables, View root):
public void setFocusables(List<View> focusables, View root) {
mRoot = root;
for (int i = 0; i < focusables.size(); ++i) {
mOriginalOrdinal.put(focusables.get(i), i);
}
for (int i = focusables.size() - 1; i >= 0; i--) {
final View view = focusables.get(i);
final View next = mNextFocusGetter.get(mRoot, view);
if (next != null && mOriginalOrdinal.containsKey(next)) {
mNextFoci.put(view, next);
mIsConnectedTo.add(next);
}
}
for (int i = focusables.size() - 1; i >= 0; i--) {
final View view = focusables.get(i);
final View next = mNextFoci.get(view);
if (next != null && !mIsConnectedTo.contains(view)) {
setHeadOfChain(view);
}
}
}
private void setHeadOfChain(View head) {
for (View view = head; view != null; view = mNextFoci.get(view)) {
final View otherHead = mHeadsOfChains.get(view);
if (otherHead != null) {
if (otherHead == head) {
return; // This view has already had its head set properly
}
// A hydra -- multi-headed focus chain (e.g. A->C and B->C)
// Use the one we've already chosen instead and reset this chain.
view = head;
head = otherHead;
}
mHeadsOfChains.put(view, head);
}
}
第一个for循环,将focusables中的所有控件都加入mOriginalOrdinal(ArrayMap对象)中,mOriginalOrdinal中的key是View值,value是对应的在focusables中的序号。
View控件可以通过布局文件中的nextFocusForward属性设置查找方向为FOCUS_FORWARD的下一个控件的id。如果下一个控件还有该属性,可以形成一个链。例如,A控件nextFocusForward是B控件的id,B控件nextFocusForward是C控件的id,这个链条就是A——>B——>C。 第二个、三个for循环,就是为了获取链条上的控件的源头控件。其中的值就存储在mHeadsOfChains(ArrayMap对象)中,像刚才这个链条A——>B——>C,就可以得到mHeadsOfChains.get(A)=A,mHeadsOfChains.get(B)=A,mHeadsOfChains.get(C)=A。但是B得在focusables中存在,并且在focusables中没有其他控件链条指向A。我们知道focusables中是root容器包含的可获取焦点的控件,具体可以参考前面,所以B得是root容器中可以获取焦点的子孙控件,并且root容器中可以获取焦点的子孙控件的nextFocusForward不能是A(A得是链条的开始)。
还有一种情况,就是代码注释中举得例子的那种A——>C and B——>C,像这种情况,放到mHeadsOfChains的最终情况就是mHeadsOfChains.get(A)=A,mHeadsOfChains.get(B)=A,mHeadsOfChains.get(C)=A。跟着代码走一遍即可得到这样的结果。像这种两个控件的nextFocusForward都指向同一个控件的情况,经过第二个、三个for循环,被指向的那个控件的源头是在focusables中排序靠前的那个指向它的控件,排序靠后的那个指向的控件的源头也变成了排序靠前的那个控件。
上面的代码就是我说的这情况,可以根据代码走一遍流程。得到了这些链条上控件的源头,是为了下面排序使用,看下排序的代码:
public int compare(View first, View second) {
if (first == second) {
return 0;
}
// Order between views within a chain is immaterial -- next/previous is
// within a chain is handled elsewhere.
View firstHead = mHeadsOfChains.get(first);
View secondHead = mHeadsOfChains.get(second);
if (firstHead == secondHead && firstHead != null) {
if (first == firstHead) {
return -1; // first is the head, it should be first
} else if (second == firstHead) {
return 1; // second is the head, it should be first
} else if (mNextFoci.get(first) != null) {
return -1; // first is not the end of the chain
} else {
return 1; // first is end of chain
}
}
boolean involvesChain = false;
if (firstHead != null) {
first = firstHead;
involvesChain = true;
}
if (secondHead != null) {
second = secondHead;
involvesChain = true;
}
if (involvesChain) {
// keep original order between chains
return mOriginalOrdinal.get(first) < mOriginalOrdinal.get(second) ? -1 : 1;
} else {
return 0;
}
}
排序的时候,先判断比较的两个控件的源头相等并且不为null的情况(处于同一条链上的控件),
1、第一个控件等于它的源头控件,则第一个控件排在前面(对应的情况:第一个控件是链的开头)
2、1不满足的情况,第二个控件等于它的源头文件,则将比较的两个控件调换位置(对应的情况:第二个控件是链的开头)。
3、1和2都不满足的情况,检查第一个控件设置nextFocusForward属性,并且指向了root容器中的子孙控件,则第一个控件排在前面(对应的情况:第一个控件不是链的开头,也不是结尾)。
4、其他情况,则将比较的两个控件调换位置(对应的情况:第一个控件是链的结尾)。
下面再比较的就是不处于同一条链上的控件,可知如果控件存在源头控件,就拿源头控件在focusables中出现的序号来代替控件本身的序号进行比较。从这条可以知道,排完序之后,可能会对原来不在链条上的控件的顺序产生影响。例如,focusables中的控件的顺序依次是:A、B、C、D,其中A——>C——>D,那么经过这种排序之后,他们的顺序就变成了A、C、D、B。
查找方向为FOCUS_FORWARD或者FOCUS_BACKWARD的排序说完了,得看看具体怎么查出来控件了,先看FOCUS_FORWARD的查找方法:
private static View getNextFocusable(View focused, ArrayList<View> focusables, int count) {
if (focused != null) {
int position = focusables.lastIndexOf(focused);
if (position >= 0 && position + 1 < count) {
return focusables.get(position + 1);
}
}
if (!focusables.isEmpty()) {
return focusables.get(0);
}
return null;
}
这个逻辑就很清晰了,找到focusables中focused控件的下一个位置的控件。如果没找到下一个位置,例如focused不存在focusables中或在focusables中最后一个位置,或者focused 为null,这个时候,只要focusables不空,都返回第一个位置的控件。最后,focusables为空的情况下,会返回null。
先看FOCUS_FORWARD的查找方法:
private static View getPreviousFocusable(View focused, ArrayList<View> focusables, int count) {
if (focused != null) {
int position = focusables.indexOf(focused);
if (position > 0) {
return focusables.get(position - 1);
}
}
if (!focusables.isEmpty()) {
return focusables.get(count - 1);
}
return null;
}
这个也是,找到focusables中focused控件的上一个位置的控件。如果没找到下一个位置,例如focused不存在focusables中或在focusables中第一个位置,或者focused 为null,这个时候,只要focusables不空,都返回最后一个位置的控件。最后,focusables为空的情况下,会返回null。
这样就将用户没有特定指明的下一个控件的情况分析完了。