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

查找最近的可以获取焦点的控件的方法focusSearch(@FocusRealDirection int direction)

  查找最近的可以获取焦点的控件是通过View类的focusSearch(@FocusRealDirection int direction)方法实现,代码如下:

    /**
     * Find the nearest view in the specified direction that can take focus.
     * This does not actually give focus to that view.
     *
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
     *
     * @return The nearest focusable in the specified direction, or null if none
     *         can be found.
     */
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }

该方法通过获取焦点的控件调用,来寻找一个最接近的指定方向上的可以获取焦点的控件。父控件不为null的时候调用mParent.focusSearch(this, direction),大部分情况下,mParent 都是ViewGroup类型的,所以看下ViewGroup类型的focusSearch(View focused, int direction)方法:

    @Override
    public View focusSearch(View focused, int direction) {
        if (isRootNamespace()) {
            // root namespace means we should consider ourselves the top of the
            // tree for focus searching; otherwise we could be focus searching
            // into other tabs.  see LocalActivityManager and TabHost for more info.
            return FocusFinder.getInstance().findNextFocus(this, focused, direction);
        } else if (mParent != null) {
            return mParent.focusSearch(focused, direction);
        }
        return null;
    }

如果当前控件是isRootNamespace(),则会调用FocusFinder类的findNextFocus()方法,如果不是则会通过mParent调用父控件的focusSearch()方法。isRootNamespace()就是检查PFLAG_IS_ROOT_NAMESPACE标识是否存在,一般就是DecorView类控件mDecor具有PFLAG_IS_ROOT_NAMESPACE标识。所以在这里就是通过mParent父控件链,一直到DecorView类控件mDecor,开始调用FocusFinder类的findNextFocus()方法,如下:

    public final View findNextFocus(ViewGroup root, View focused, int direction) {
        return findNextFocus(root, focused, null, direction);
    }

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
        if (focused != null) {
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList<View> focusables = mTempList;
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);
            if (!focusables.isEmpty()) {
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }    

该方法所做操作:
  1、调用getEffectiveRoot(root, focused)得到实际的查找根控件
  2、调用findNextUserSpecifiedFocus(effectiveRoot, focused, direction)得到用户特定指明的下一个控件,如果用户指明了,就返回该控件。
  3、如果用户没有特定指明的下一个控件,需要根据一定的规则找到下一个可以获取焦点的控件。最后返回找到的控件作为结果。
  该篇先说前两个步骤的相关代码。第3步看下一篇文章。

getEffectiveRoot(root, focused)

  看一下getEffectiveRoot(root, focused)方法代码:

    private ViewGroup getEffectiveRoot(ViewGroup root, View focused) {
        if (focused == null || focused == root) {
            return root;
        }
        ViewGroup effective = null;
        ViewParent nextParent = focused.getParent();
        do {
            if (nextParent == root) {
                return effective != null ? effective : root;
            }
            ViewGroup vg = (ViewGroup) nextParent;
            if (vg.getTouchscreenBlocksFocus()
                    && focused.getContext().getPackageManager().hasSystemFeature(
                            PackageManager.FEATURE_TOUCHSCREEN)
                    && vg.isKeyboardNavigationCluster()) {
                // Don't stop and return here because the cluster could be nested and we only
                // care about the top-most one.
                effective = vg;
            }
            nextParent = nextParent.getParent();
        } while (nextParent instanceof ViewGroup);
        return root;
    }

参数root就是DecorView类控件mDecor,该方法是检查如果目前获取焦点的控件处于键盘导航键区之内,那么实际查找的根控件可能是键区的根控件。键区的根控件还需要满足以下条件,容器控件应该忽略它本身及子控件的焦点请求和设备有一个触摸屏。如果满足这个条件,就将键区的根控件返回,并且键区是可能嵌套的,所以需要最外层的满足条件的键区根控件。如果没找到,就将参数root返回。

findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction)

  再看第2步骤中的findNextUserSpecifiedFocus(effectiveRoot, focused, direction)方法:

    private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
        // check for user specified next focus
        View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
        View cycleCheck = userSetNextFocus;
        boolean cycleStep = true; // we want the first toggle to yield false
        while (userSetNextFocus != null) {
            if (userSetNextFocus.isFocusable()
                    && userSetNextFocus.getVisibility() == View.VISIBLE
                    && (!userSetNextFocus.isInTouchMode()
                            || userSetNextFocus.isFocusableInTouchMode())) {
                return userSetNextFocus;
            }
            userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
            if (cycleStep = !cycleStep) {
                cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
                if (cycleCheck == userSetNextFocus) {
                    // found a cycle, user-specified focus forms a loop and none of the views
                    // are currently focusable.
                    break;
                }
            }
        }
        return null;
    }

先通过focused.findUserSetNextFocus(root, direction)获得用户指定的下一个方向的控件。但是该控件需要满足一定条件,才能作为结果返回。while循环里面可以看到具体条件,控件是可获得焦点的(isFocusable()),控件是可见的,控件不是在触摸模式(Android 控件获取焦点 文章中解释)或者在触摸模式下可以获得焦点。满足这几个条件,该控件就作为结果返回。如果不满足这几个结果,就查找找到的控件的用户指定的下一个方向的控件。再检查它是否满足以上条件,如果满足就作为结果返回。其中变量cycleStep的作用是为了避免用户指定的控件形成了一个循环,并且都不满足获取控件焦点的条件,如果发现循环了,会跳出循环,返回null。
  再看下focused.findUserSetNextFocus(root, direction)方法看是怎么获得用户指定的下一个方向的控件,如下:

    View findUserSetNextFocus(View root, @FocusDirection int direction) {
        switch (direction) {
            case FOCUS_LEFT:
                if (mNextFocusLeftId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusLeftId);
            case FOCUS_RIGHT:
                if (mNextFocusRightId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusRightId);
            case FOCUS_UP:
                if (mNextFocusUpId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusUpId);
            case FOCUS_DOWN:
                if (mNextFocusDownId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusDownId);
            case FOCUS_FORWARD:
                if (mNextFocusForwardId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusForwardId);
            case FOCUS_BACKWARD: {
                if (mID == View.NO_ID) return null;
                final View rootView = root;
                final View startView = this;
                // Since we have forward links but no backward links, we need to find the view that
                // forward links to this view. We can't just find the view with the specified ID
                // because view IDs need not be unique throughout the tree.
                return root.findViewByPredicateInsideOut(startView,
                    t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId)
                            == startView);
            }
        }
        return null;
    }

从这里可以看到参数direction的值可能为FOCUS_LEFT、FOCUS_RIGHT、FOCUS_UP、FOCUS_DOWN、FOCUS_FORWARD、FOCUS_BACKWARD,它们分别对应用户按导航键KeyEvent.KEYCODE_DPAD_LEFT、KeyEvent.KEYCODE_DPAD_RIGHT、KeyEvent.KEYCODE_DPAD_UP、KeyEvent.KEYCODE_DPAD_DOWN、KeyEvent.KEYCODE_TAB、KeyEvent.KEYCODE_TAB+( KEYCODE_SHIFT_LEFT或KEYCODE_SHIFT_RIGHT)触发的方向值。对应关系如下表,最后一列xml布局文件设置属性,是控件的成员变量mNextFocusXXId,在布局文件中对应属性。该属性是一个ID int值,后面就可以通过ID值找到对应的控件。

导航按键寻找方向xml布局文件设置属性
KEYCODE_DPAD_LEFTFOCUS_LEFTnextFocusLeft
KEYCODE_DPAD_RIGHTFOCUS_RIGHTnextFocusRight
KEYCODE_DPAD_UPFOCUS_UPnextFocusUp
KEYCODE_DPAD_DOWNFOCUS_DOWNnextFocusDown
KEYCODE_TABFOCUS_FORWARDnextFocusForward
KEYCODE_TAB+( KEYCODE_SHIFT_LEFT或KEYCODE_SHIFT_RIGHT)FOCUS_BACKWARD

  在该方法里,前五个方向导航的代码都是相似的,是先检查对应的属性id是否已经设置了,如果没设置,就返回null。如果设置了,就执行findViewInsideOutShouldExist()方法,通过设置的id值找到对应的控件。
  在参数direction是FOCUS_BACKWARD时,是检查控件本身是否设置了ID值,如果没有设置,也返回null。如果设置了mID,则执行root.findViewByPredicateInsideOut()方法来查找。从列表看到,发生FOCUS_BACKWARD方法的查找的时候,是没有设置对应的属性的,这个是什么意思呢,这个是为了寻找设置了nextFocusForward的值等于当前焦点控件的控件,就是xml布局文件中nextFocusForward的id的值是当前焦点控件的控件。

findViewInsideOutShouldExist(View root, int id)

  先看看findViewInsideOutShouldExist()方法:

    private View findViewInsideOutShouldExist(View root, int id) {
        return findViewInsideOutShouldExist(root, this, id);
    }

    private View findViewInsideOutShouldExist(View root, View start, int id) {
        if (mMatchIdPredicate == null) {
            mMatchIdPredicate = new MatchIdPredicate();
        }
        mMatchIdPredicate.mId = id;
        View result = root.findViewByPredicateInsideOut(start, mMatchIdPredicate);
        if (result == null) {
            Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
        }
        return result;
    }

    public final <T extends View> T findViewByPredicateInsideOut(
            View start, Predicate<View> predicate) {
        View childToSkip = null;
        for (;;) {
            T view = start.findViewByPredicateTraversal(predicate, childToSkip);
            if (view != null || start == this) {
                return view;
            }

            ViewParent parent = start.getParent();
            if (parent == null || !(parent instanceof View)) {
                return null;
            }

            childToSkip = start;
            start = (View) parent;
        }
    }

findViewInsideOutShouldExist()方法,会去调用root.findViewByPredicateInsideOut(start, mMatchIdPredicate)方法,这里的start就是咱们现在获取到的焦点的控件,是通过前面传递过来的。进入findViewByPredicateInsideOut()方法,会首先调用参数start的findViewByPredicateTraversal(predicate, childToSkip),start如果是View类型的,就会调用View类的findViewByPredicateTraversal(),代码如下:

    protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
            View childToSkip) {
        if (predicate.test(this)) {
            return (T) this;
        }
        return null;
    }

可见满足predicate的test()方法,就可以,这个就是当前控件的id和predicate类的成员id值相等。如果相等,就返回该控件,不满足,就返回null。
如果返回不为null,或者start也就是当前调用findViewByPredicateInsideOut()方法的控件(代表已经沿着父控件链到达顶层祖先控件),就返回View。这俩条件都不满足,就会找到它的父控件,并且将当前控件设置为childToSkip,该变量代表着需要跳过的控件,因为已经必过了。父控件大多数都是ViewGroup类型的,看一下ViewGroup类的findViewByPredicateTraversal():

    @Override
    protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
            View childToSkip) {
        if (predicate.test(this)) {
            return (T) this;
        }

        final View[] where = mChildren;
        final int len = mChildrenCount;

        for (int i = 0; i < len; i++) {
            View v = where[i];

            if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
                v = v.findViewByPredicate(predicate);

                if (v != null) {
                    return (T) v;
                }
            }
        }

        return null;
    }

可见是先比较当前控件的id,如果满足就返回了。如果不满足,需要比较它的子控件的id,并且会跳过之前比较过的子控件。可见,root.findViewByPredicateInsideOut(start, mMatchIdPredicate)会把参数root包含的控件都进行比较。如果都没有被找到,会返回null。

root.findViewByPredicateInsideOut(View start, Predicate predicate)

  该方法对应查找方向为FOCUS_BACKWARD的执行方法,

    public final <T extends View> T findViewByPredicateInsideOut(
            View start, Predicate<View> predicate) {
        View childToSkip = null;
        for (;;) {
            T view = start.findViewByPredicateTraversal(predicate, childToSkip);
            if (view != null || start == this) {
                return view;
            }

            ViewParent parent = start.getParent();
            if (parent == null || !(parent instanceof View)) {
                return null;
            }

            childToSkip = start;
            start = (View) parent;
        }
    }

可见它和上面findViewInsideOutShouldExist(View root, int id)代码是相似的,不过就是判断条件predicate是不同的,FOCUS_BACKWARD的判断条件是t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId) == startView,这个就是寻找root中的控件的mNextFocusForwardId的值等于当前获取焦点的ID的控件。
  这样findNextFocus()的第2步的findNextUserSpecifiedFocus(effectiveRoot, focused, direction)方法就完毕了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值