bug-parameter must be a descendant of this view

附图是错误日志:


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;
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • 最近在微信或whatsapp 得悉不少朋友或家人身体抱恙,从简单的小手术,到嚴重的长期病患,甚至去世的消息。听到后...
    甘於平淡的学霸阅读 611评论 1 6
  • 一些简单的游戏可以用自定义控件实现,如拼图游戏。先上效果图: 1、游戏的大概思路 游戏的基本思路:将一个大图切割成...
    AxeChen阅读 4,767评论 3 38
  • 青涩的年华褪去了 但愿我已不再是小小少年 梧桐落地了秋雨 天空装扮着明眸 如花美眷的青春 仓促地像教堂里冗长的钟声...
    荒田半亩阅读 232评论 2 5
  • 周末本想在家养养皮肤的,可是被一阵电话催醒让买三叶参和白英两种药材,就下床捯饬一番去了中医药附属医院那边挨个的问是...
    兖兒阅读 209评论 0 0