Android 查找最近的可以获取焦点的控件(二) 未指明控件下一个查找属性的查找

该篇文章承接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。
  这样就将用户没有特定指明的下一个控件的情况分析完了。

上一篇:C#学习-魔塔项目记录(2021.11.30)


下一篇:Arduino - 看门狗定时器(WDT:Watch Dog Timer)