RecyclerView的Item沒有充滿整個寬度

  • 概述

    在一開始使用RecyclerView的過程中,可能會遇到這么一種情況,就是我們的item View已經(jīng)設(shè)置成match_parent了,RecyclerView也設(shè)置成match_parent,但是在顯示的時候卻只顯示內(nèi)容wrap_content的大小,布局文件沒有問題,那么么問題出在哪呢?

    分析一下,可以猜測,只有在item添加到RecyclerView時的中間部分才有可能修改LayoutParams,也就是問題最有可能出現(xiàn)在onCreateViewHolder中的inflate的時候。

  • View.inflate

    通常為了簡單,我們經(jīng)常調(diào)用View.inflate來加載布局,而我們出現(xiàn)上面問題的時候也是使用的這個,這個方法定義如下:

    public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
        LayoutInflater factory = LayoutInflater.from(context);
        return factory.inflate(resource, root);
    }
    

    因為我們是要加到RecyclerView中去,在RecyclerView的布局工作中會調(diào)用addView添加,所以我們這里的root必須傳null,否則就會拋出“不允許有多個parent”的異常。

    可以看到,這里其實就是對于LayoutInflater.from(context).inflate(@LayoutRes int resource, @Nullable ViewGroup root)方法的封裝:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    

    這個方法又是調(diào)用了三個參數(shù)的inflate重載方法:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                  + Integer.toHexString(resource) + ")");
        }
    
        View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
        if (view != null) {
            return view;
        }
        XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
    

    因為我們這里傳入的root是null,所以attachToRoot為false,這里調(diào)用了tryInflatePrecompiled方法創(chuàng)建View:

    private @Nullable
    View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root,
        boolean attachToRoot) {
        if (!mUseCompiledView) {
            return null;
        }
    
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
    
        // Try to inflate using a precompiled layout.
        String pkg = res.getResourcePackageName(resource);
        String layout = res.getResourceEntryName(resource);
    
        try {
            Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader);
            Method inflater = clazz.getMethod(layout, Context.class, int.class);
            View view = (View) inflater.invoke(null, mContext, resource);
    
            if (view != null && root != null) {
                // We were able to use the precompiled inflater, but now we need to do some work to
                // attach the view to the root correctly.
                XmlResourceParser parser = res.getLayout(resource);
                try {
                    AttributeSet attrs = Xml.asAttributeSet(parser);
                    advanceToRootNode(parser);
                    ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
    
                    if (attachToRoot) {
                        root.addView(view, params);
                    } else {
                        view.setLayoutParams(params);
                    }
                } finally {
                    parser.close();
                }
            }
    
            return view;
        } catch (Throwable e) {
            if (DEBUG) {
                Log.e(TAG, "Failed to use precompiled view", e);
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        return null;
    }
    

    可以看到,這里利用反射,找到View類中View(Context.class, int.class)這個構(gòu)造函數(shù)來生成View實例,而View的構(gòu)造函數(shù)中并沒有任何關(guān)于LayoutParams的操作,往下看,if語句判斷中null不為空這個條件不符合,所以不會執(zhí)行if語句內(nèi)的代碼,if語句內(nèi)恰好是把我們xml布局中定義的LayoutParams屬性設(shè)置到View的操作,所以,因為root為null的關(guān)系,我們在布局中設(shè)置的layout_xxx相關(guān)的屬性并沒有被使用,這里只是返回一個基本構(gòu)造函數(shù)返回的View實例。

    從這我們也能看出來,LayoutParams是有關(guān)子View在父容器中存在的效果的,所以這里的父容器為空時也不需要處理LayoutParams。但有人會說,子View本身也有需要顯示的內(nèi)容啊,如果不設(shè)置LayoutParams的話那內(nèi)容也顯示不出來了啊。是的,LayoutParams是必須要設(shè)置的,只是..不是現(xiàn)在,接著往下看。

  • RecyclerView的處理

    創(chuàng)建了View實例之后,返回onCreateViewHolder方法,這個方法是在RecyclerView的layout流程中的tryGetViewHolderForPositionByDeadline方法中調(diào)用的,它里面有這樣一段代碼:

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    

    可以看到,如果View沒有設(shè)置LayoutParams的話會調(diào)用generateDefaultLayoutParams方法設(shè)置,這個方法內(nèi)部是調(diào)用了對應(yīng)LayoutManager的同名方法,以LinearLayoutManager為例:

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }
    

    可見,這里默認(rèn)會使用WRAP_CONTENT作為寬高的LayoutParams屬性,這也就能解釋了為什么會出現(xiàn)Item沒有充滿RecyclerView寬(高)的情況。

  • 其他

    假使我們添加了DividerItemDecoration(ColorDrawable(Color.GRAY)),并且錯誤地使用了View.inflate方法加載,那么會出現(xiàn)這么一種情況:

    image-20211206144221245

    灰色的是我們添加的分割線,卡其色是Item的背景色,也就是內(nèi)容區(qū)域,可以看到,分割線被內(nèi)容擋住了一部分,這是為什么呢?而分割線為什么又是這么高呢?

    DividerItemDecoration的getItemOffsets方法如下:

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            RecyclerView.State state) {
        if (mDivider == null) {
            outRect.set(0, 0, 0, 0);
            return;
        }
        if (mOrientation == VERTICAL) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
    

    mDivider是設(shè)置的Drawable,也就是ColorDrawable,它沒有重寫Drawable的getIntrinsicHeight方法:

    public int getIntrinsicHeight() {
        return -1;
    }
    

    所以這就是高度為什么是1像素的原因。

    我們知道在RecyclerView的measure流程中會調(diào)用getItemOffsets方法把分割線的高度作為child的一部分提前預(yù)留出來,這里是-1,所以取最大范圍的值,也就是child本身設(shè)置的大小,這里在onDraw中又通過bottom-mDivider.getIntrinsicHeight()賦值給分割線的top:

    for (int i = 0; i < childCount; i++) {
        final View child = parent.getChildAt(i);
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
        final int top = bottom - mDivider.getIntrinsicHeight();
        mDivider.setBounds(left, top, right, bottom);
        mDivider.draw(canvas);
    }
    

    所以會出現(xiàn)內(nèi)容覆蓋分割線一部分的效果。

  • 總結(jié)

    根據(jù)以上分析,我們得出以下結(jié)論:

    對于RecyclerView,在加載Item布局時,我們要使用LayoutInflater.from(context).inflate(R.layout.xxxxx, parent,false)來加載,這個方法可以保證root不為null同時attchToRoot為false,也就能保證既可以應(yīng)用了我們布局中設(shè)置的layout屬性,又不會產(chǎn)生“不允許存在多個parent”的異常。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容