-
概述
在一开始使用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方法加载,那么会出现这么一种情况:
灰色的是我们添加的分割线,卡其色是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”的异常。