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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,098評論 25 708
  • 最近在微信或whatsapp 得悉不少朋友或家人身體抱恙,從簡單的小手術,到嚴重的長期病患,甚至去世的消息。聽到后...
    甘於平淡的學霸閱讀 634評論 1 6
  • 一些簡單的游戲可以用自定義控件實現,如拼圖游戲。先上效果圖: 1、游戲的大概思路 游戲的基本思路:將一個大圖切割成...
    AxeChen閱讀 4,910評論 3 38
  • 青澀的年華褪去了 但愿我已不再是小小少年 梧桐落地了秋雨 天空裝扮著明眸 如花美眷的青春 倉促地像教堂里冗長的鐘聲...
    荒田半畝閱讀 246評論 2 5
  • 周末本想在家養養皮膚的,可是被一陣電話催醒讓買三葉參和白英兩種藥材,就下床捯飭一番去了中醫藥附屬醫院那邊挨個的問是...
    兗兒閱讀 217評論 0 0