RecyclerView的Item没有充满整个宽度

  • 概述

    在一开始使用RecyclerView的过程中,可能会遇到这么一种情况,就是我们的item View已经设置成match_parent了,RecyclerView也设置成match_parent,但是在显示的时候却只显示内容wrap_content的大小,布局文件没有问题,那么么问题出在哪呢?

    分析一下,可以猜测,只有在item添加到RecyclerView时的中间部分才有可能修改LayoutParams,也就是问题最有可能出现在onCreateViewHolder中的inflate的时候。

  • View.inflate

    通常为了简单,我们经常调用View.inflate来加载布局,而我们出现上面问题的时候也是使用的这个,这个方法定义如下:

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

    因为我们是要加到RecyclerView中去,在RecyclerView的布局工作中会调用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);
    }
    

    这个方法又是调用了三个参数的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,这里调用了tryInflatePrecompiled方法创建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)这个构造函数来生成View实例,而View的构造函数中并没有任何关于LayoutParams的操作,往下看,if语句判断中null不为空这个条件不符合,所以不会执行if语句内的代码,if语句内恰好是把我们xml布局中定义的LayoutParams属性设置到View的操作,所以,因为root为null的关系,我们在布局中设置的layout_xxx相关的属性并没有被使用,这里只是返回一个基本构造函数返回的View实例。

    从这我们也能看出来,LayoutParams是有关子View在父容器中存在的效果的,所以这里的父容器为空时也不需要处理LayoutParams。但有人会说,子View本身也有需要显示的内容啊,如果不设置LayoutParams的话那内容也显示不出来了啊。是的,LayoutParams是必须要设置的,只是..不是现在,接着往下看。

  • RecyclerView的处理

    创建了View实例之后,返回onCreateViewHolder方法,这个方法是在RecyclerView的layout流程中的tryGetViewHolderForPositionByDeadline方法中调用的,它里面有这样一段代码:

    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没有设置LayoutParams的话会调用generateDefaultLayoutParams方法设置,这个方法内部是调用了对应LayoutManager的同名方法,以LinearLayoutManager为例:

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

    可见,这里默认会使用WRAP_CONTENT作为宽高的LayoutParams属性,这也就能解释了为什么会出现Item没有充满RecyclerView宽(高)的情况。

  • 其他

    假使我们添加了DividerItemDecoration(ColorDrawable(Color.GRAY)),并且错误地使用了View.inflate方法加载,那么会出现这么一种情况:

    image-20211206144221245

    灰色的是我们添加的分割线,卡其色是Item的背景色,也就是内容区域,可以看到,分割线被内容挡住了一部分,这是为什么呢?而分割线为什么又是这么高呢?

    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是设置的Drawable,也就是ColorDrawable,它没有重写Drawable的getIntrinsicHeight方法:

    public int getIntrinsicHeight() {
        return -1;
    }
    

    所以这就是高度为什么是1像素的原因。

    我们知道在RecyclerView的measure流程中会调用getItemOffsets方法把分割线的高度作为child的一部分提前预留出来,这里是-1,所以取最大范围的值,也就是child本身设置的大小,这里在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);
    }
    

    所以会出现内容覆盖分割线一部分的效果。

  • 总结

    根据以上分析,我们得出以下结论:

    对于RecyclerView,在加载Item布局时,我们要使用LayoutInflater.from(context).inflate(R.layout.xxxxx, parent,false)来加载,这个方法可以保证root不为null同时attchToRoot为false,也就能保证既可以应用了我们布局中设置的layout属性,又不会产生“不允许存在多个parent”的异常。

你可能感兴趣的:(RecyclerView的Item没有充满整个宽度)