该篇文章承接Android 查找最近的可以获取焦点的控件(一)
将其相关代码摘录如下:
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来获得下一个控件。
先看看将可获取焦点的控件添加到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)
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赋值部分,参数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。
这样就将用户没有特定指明的下一个控件的情况分析完了。