美文网首页bug集
bug-parameter must be a descenda

bug-parameter must be a descenda

作者: 烬日沉香 | 来源:发表于2018-06-11 14:33 被阅读586次

    附图是错误日志:


    image.png

    该异常抛出有一定的前提:
    compile 'com.android.support:recyclerview-v7:23.0.0'
    RecyclerView的23版本没有问题, 24或25版本会抛出异常。
    23以上的版本中RecyclerView中的代码已经不相同。
    而23及下的RecyclerView中没有isPreferredNextFocus方法,其focusSearch()方法内部也不一样。高版本在focusSearch()方法内return后调用了isPreferredNextFocus()。

    25版本的RecyclerView.focusSearch()

    public View focusSearch(View focused, int direction) {
        View result = mLayout.onInterceptFocusSearch(focused, direction);
        if (result != null) {
            return result;
        }
        final boolean canRunFocusFailure = mAdapter != null && mLayout != null
                && !isComputingLayout() && !mLayoutFrozen;
    
        final FocusFinder ff = FocusFinder.getInstance();
        if (canRunFocusFailure
                && (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {
            //此处省略N行........
            if (needsFocusFailureLayout) {
                //此处省略N行........
               }
        } else {
          //此处省略N行.........
        }
        return isPreferredNextFocus(focused, result, direction)
                ? result : super.focusSearch(focused, direction);
    }
    

    需先判断isPrfeeredNextFocus是否为true,异常在该方法内部调到的ViewGroup内部的offsetRectBetweenParentAndChild抛出异常。

    23版本的ReyclerView.focusSearch()

    @Override
    public View focusSearch(View focused, int direction) {
        View result = mLayout.onInterceptFocusSearch(focused, direction);
        if (result != null) {
            return result;
        }
        final FocusFinder ff = FocusFinder.getInstance();
        result = ff.findNextFocus(this, focused, direction);
        if (result == null && mAdapter != null && mLayout != null && !isComputingLayout()
                && !mLayoutFrozen) {
            eatRequestLayout();
            result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
            resumeRequestLayout(false);
        }
        return result != null ? result : super.focusSearch(focused, direction);
    }
    

    高版本的isPreferredNextFocus()方法,内部又继续调用了isPreferredNextFocusAbsolute(),内部又调用了ViewGrruop的offsetDescendantRectToMyCoords()方法,抛出异常的位置的的内部调用的ViewGruop(而此时的ViewGruop实际上是指RecyclerView)的offsetRectBetweenParentAndChild()。

    接下来看一下抛出异常的位置的具体原因:
    断点在此处看一下:入参descendant是DecorView,

    image

    获取descendant的parent时,得到的是ViewRootImpl


    image

    此时判断descendant的parent是否是Recycler,不是,因此抛出了异常。
    那么问题来了,这个方法时在做什么?
    事实上这个方法是在判断,入参中的的View,即descendant,是否是RecyclerView中的View。
    通过获取其父parent(源码中会通过一个while循环不断地去取parent),是否是RecyclerView,即(theParent == this),此处调用时是RecyclerView,即ViewGroup类内的this,就是指RecyclerView。
    为什么得到的parent不是RecyclerView?这一定是入参descendant就已经错了。
    来看一下作者君的上层UI部分的代码(具体还是得分析个人的上层代码,作者君只能提供一下思路。)
    处于业务需要,当时作者君重写了LinerLayoutManager的onFocusSearchFailed()方法,指定在寻找焦点失败的时候返回一个view。


    image
    onFocusSearchFailed()也在RecyclerView的focusSearch()内,我们来稍微回顾下:
    public View focusSearch(View focused, int direction) {
            View result = mLayout.onInterceptFocusSearch(focused, direction);
            if (result != null) {
                return result;
            }
            final FocusFinder ff = FocusFinder.getInstance();
            if (canRunFocusFailure
                    && (direction == View.FOCUS_FORWARD || 
                      direction == View.FOCUS_BACKWARD)) {
                 boolean needsFocusFailureLayout = false;
                if (mLayout.canScrollVertically()) {
                    final int absDir =
                  direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
                    final View found = ff.findNextFocus(this, focused, absDir);
                    needsFocusFailureLayout = found == null;
                }
            //此处省略N行...........
                result = ff.findNextFocus(this, focused, direction);
            } else {
                result = ff.findNextFocus(this, focused, direction);
                if (result == null && canRunFocusFailure) {
                   //此处省略N行.........
               //可在LinerLayoutManager内重写该方法。
                    result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);
                    resumeRequestLayout(false);
                }
            }
            return isPreferredNextFocus(focused, result, direction)
                    ? result : super.focusSearch(focused, direction);
        }
    

    作者君在寻找焦点失败的时候,回调的这个onFocusSearchFailed内,手动返回了一个指定的View是CountLayout(即下图中的数量这一行。)
    该图中的商品属性:几人坐、颜色分类是Proplayout,内部通过TextView+RecyclerView来展示商品的属性。之所以指定是为了解决,
    在颜色分类按向下键,需要先聚焦到数量这一行,让用户先选择商品数量,但实际上却发现,如果当前焦点在【嫩绿色[绒布]】这个属性上时,按向下键时,焦点跳过数量,而跳转到了【下单】按钮上。因此重写了在寻找焦点失败的onFocusSearchFailed()方法。因为此时RecyclerView的向下的方向上已经没有可以聚焦的view,所以会回调此方法。而在此处指定下一个聚焦的是CountLayout(数量这一行可聚焦的view),看起来是个不错的解决方案。


    image.png

    然而由于RecyclerView高版本的源码中在返回view时,添加了严格的检查机制,需要判断指定的view是否是RecyclerView内部的view:
    return isPreferredNextFocus(focused, result, direction)
    ? result : super.focusSearch(focused, direction);
    所以抛出了异常。

    那么应该怎么做呢?为了解决向下按键跳行的问题,我是否应该考虑继续在onFocusSearchFailed()内部做改动,可是这意味着还是会执行isPreferredNextFocus(),还是会抛出异常。换个方法,不让代码执行到这里呢?
    如果了解focusSearch的源码,你会发现一开始有个直接return的方法如下:
    当onInterceptFocusSearch返回view时,后续的寻找焦点、判断是否符合条件等等代码都不再继续往下执行。

    public View focusSearch(View focused, int direction) {
            View result = mLayout.onInterceptFocusSearch(focused, direction);
            if (result != null) {
                return result;
            }
            final FocusFinder ff = FocusFinder.getInstance();
          //此处省略N行.........
            return isPreferredNextFocus(focused, result, direction)
                    ? result : super.focusSearch(focused, direction);
        }
    

    所以作者君采用重写onInterceptFocusSearch():
    具体的解决方案跟作者君的UI布局有非常大的关系,读者需要根据自己的UI架构去更改。
       获取其父view,再遍历所有的child,如果类型是否是PropLayout,判断其下一个是否不是PropLayout。以此获取就是CountLayout这个child。
           因为作者君的布局是ScrollView内动态添加PropLayout(TextView+RecyclerView)和CountLayout
       

    inearLayoutManager=new LinearLayoutManager(mContext){
        @Override
        public View onInterceptFocusSearch(View focused, int direction) {
            switch (direction){
                case View.FOCUS_DOWN:
                    if (getParent() instanceof  ViewGroup){
                        ViewGroup group= (ViewGroup) getParent();
                        for (int i=0;i<group.getChildCount();i++){
                            if (group.getChildAt(i)==PropLayout.this){
                                if (i+1<group.getChildCount()&&!(group.getChildAt(i+1) instanceof PropLayout)){
                                    //默认设置为第1项,当countlayout内布局发生改变时,需要重新改造此段代码
                                    ViewGroup viewGroup=(ViewGroup) group.getChildAt(i+1);
                                          return viewGroup;
                                }
                            }
                        }
                    }
                    break;
            }
            return super.onInterceptFocusSearch(focused, direction);
        }
    

    此处需要注意的是,如果返回的view是ViewGroup对象,其内部必须有可以获取焦点的view,否则还是失败的。因为在RootViewImp内会遍历其子view,若获取不到,就返回上一层。去寻找其他可以获取焦点的viw。最终还是跳转到了【下单】按钮。
    焦点查询是从RootVieImpl进入的:

        private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;
      ...............此处省略N行代码,自行看源码...............
            if (direction != 0) {
                View focused = mView.findFocus();
                if (focused != null) {
             //此处就是进入到view,再进入到Recyclerview内focusSearch的方法源头。
                    View v = focused.focusSearch(direction);
                    if (v != null && v != focused) 
                   //若RecyclerView内被打断,并且返回一个View。则进入以下判断。
                        focused.getFocusedRect(mTempRect);
                     if (mView instanceof ViewGroup) {
                            ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                    focused, mTempRect);
                            ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                    v, mTempRect);
                        }
                        //关键方法在这里!!!重要重要重要!!!!重要的事情说三遍
                   //只要这里返回true,寻找焦点则结束,那么其他view则不再有机会
                        if (v.requestFocus(direction, mTempRect)) {
                            playSoundEffect(SoundEffectConstants
                                    .getContantForFocusDirection(direction));
                            return FINISH_HANDLED;
                        }
                    }
                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    // find the best view to give focus to in this non-touch-mode with no-focus
                    View v = focusSearch(null, direction);
                    if (v != null && v.requestFocus(direction)) {
                        return FINISH_HANDLED;
                    }
                }
            }
        }
        return FORWARD;
    }
    
    

    进入的标记是FOCUS_AFTER_DESCENDANTS,主要看onRequestFocusInDescendants方法返回情况,
    若在这一步结束,那么就不会super。这一步进入ViewGroup的requestFocus()方法。

    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        int descendantFocusability = getDescendantFocusability();
    
        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
            }
            case FOCUS_AFTER_DESCENDANTS: {
                final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
                return took ? took : super.requestFocus(direction, previouslyFocusedRect);
            }
            default:
                throw new IllegalStateException("descendant focusability must be "
                        + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
                        + "but is " + descendantFocusability);
        }
    }
    

    这个方法内遍历了内部是否有可以获取焦点的view。(因为CountLayout传入了第2个子View,是LinearLayout,其内部的子view都不可获取焦点,所以此方法返回了false。那么上诉代码就变成了super,最终没有获取到焦点,跳转到外部,寻找向下方向可以获取焦点的view,最终聚焦到【下单】按钮)

    protected boolean onRequestFocusInDescendants(int direction,
            Rect previouslyFocusedRect) {
        int index;
        int increment;
        int end;
        int count = mChildrenCount;
        if ((direction & FOCUS_FORWARD) != 0) {
            index = 0;
            increment = 1;
            end = count;
        } else {
            index = count - 1;
            increment = -1;
            end = -1;
        }
        final View[] children = mChildren;
        for (int i = index; i != end; i += increment) {
            View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                if (child.requestFocus(direction, previouslyFocusedRect)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    

    相关文章

      网友评论

        本文标题:bug-parameter must be a descenda

        本文链接:https://www.haomeiwen.com/subject/gwxheftx.html