问题复现
为什么发现了这个问题呢?是当时要写一个列表,列表本来很简单,一行显示一个文本,实现起来也很容易,一个RecyclerView就搞定。
Activity以及Adapter代码如下:
private void initView() {
mRecyclerView = (RecyclerView) findViewById(R.id.rv_inflate_test);
RVAdapter adapter = new RVAdapter();
adapter.setData(mockData());
LinearLayoutManager manager = new LinearLayoutManager(this);
manager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
mRecyclerView.setLayoutManager(manager);
mRecyclerView.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
private List<String> mockData(){
List<String> datas = new ArrayList<>();
for(int i=0;i<100;i++){
datas.add("这是第"+i+ "个item ");
}
return datas;
}
public static class RVAdapter extends RecyclerView.Adapter{
private List<String> mData;
public void setData(List<String> data) {
mData = data;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new InflateViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null));
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
InflateViewHolder viewHolder = (InflateViewHolder) holder;
((InflateViewHolder) holder).mTextView.setText(mData.get(position));
}
@Override
public int getItemCount() {
return mData == null ? 0:mData.size();
}
public static class InflateViewHolder extends RecyclerView.ViewHolder{
private TextView mTextView;
public InflateViewHolder(View itemView) {
super(itemView);
mTextView = (TextView) itemView.findViewById(R.id.text_item);
}
}
}
然后RecyclerView的item布局文件如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/text_item"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="#AA47BC"
android:gravity="center"
/>
</LinearLayout>
代码很简单,就是一个RecyclerView 显示一个简单的列表,一行显示一个文本。写完代码运行看一下效果:
运行效果一看,这是什么鬼?右边空出来这么大一块?一看就觉得是item的布局写错了,难道item的宽写成wrap_content
? 那就去改一下嘛。进入item布局一看:
不对啊,明明布局的宽写的是match_parent,为什么运行的结果就是包裹内容的呢?然后就想着既然LinearLayout作为根布局宽失效了,那就换其他几种布局方式试一下呢?根布局换成RelativeLayout,其他不变:
看一下运行效果:
换成RelativeLayout后,运行的效果,好像就是我们想要的了,曾经一度以后只要将跟布局换成RelativeLayout,就没有宽高失效的问题了。为了验证这个问题,我改变了高度再来测试,如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_red_light"
>
<TextView
android:id="@+id/text_item"
android:layout_width="match_parent"
android:layout_height="50dp"
android:textSize="18sp"
android:textColor="@android:color/white"
android:background="#AA47BC"
android:gravity="center"
/>
将布局的宽和高固定一个确定的值200dp,然后再来看一下运行效果。
如上,并没有什么卵用,宽和高都失效了。然后又在固定宽高的情况下将布局换为原来的LinearLayout和FrameLayout,效果和前面一样,包裹内容。
因此,不管用什么布局作为根布局都会出现宽高失效的问题,那就得另找原因。到底是什么原因呢?想到以前写了这么多的列表,也没有出现宽高失效的问题啊?于是就去找以前的代码来对比一下:
通过对比,发现宽高失效与不失效的区别在与Adapter中创建ViewHolder是加载布局的方式不同:
这种加载方式Item宽高失效。
LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,null)
这种方式加载布局item不会出现宽高失效。
LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
问题我们算是定位到了,就是加载布局的方式不一样,那么这两种加载布局的写法到底有什么区别呢?这个我们就需要去深入了解inflate这个方法了。
inflate 加载布局几种写法的区别
上面我们定位到了RecyclerView item 布局宽高失效的原因在于使用inflate 加载布局时的问题,对于inflate 加载布局的几种写法最终都指向LayoutInflater.from(Context context).inflate(@LayoutRes int resource, ViewGroup root, boolean attachToRoot);
具体的区别可以参考另一篇文章Android inflate方法的分析
参数解释:
- resource:加载的布局文件资源id,如:R.layout.main_page。
- root:如果attachToRoot(也就是第三个参数)为true, 那么root就是为新加载的View指定的父View。否则,root只是一个为返回View层级的根布局提供LayoutParams值的简单对象。
- attachToRoot: 新加载的布局是否添加到root,如果为false,root参数仅仅用于为xml根布局创建正确的LayoutParams子类(列如:根布局为LinearLayout,则用LinearLayout.LayoutParam)。
源码分析宽高失效原因
1.首先我们来分析一下inflate 方法的源码:
....
//前面省略
//result是最终返回的View
View result = root;
try {
...
// 省略部分代码
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 重点就在这个else代码块里了
//解释1:首先创建了xml布局文件的根View,temp View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
// 解释2:判断root是否为null,不为null,就通过root生成LayoutParams
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
// 解释3:如果在root不为null, 并且attachToRoot为false,就为temp View(也就是通过inflate加载的根View)设置LayoutParams.
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//解释4:加载根布局temp View 下面的子View
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
//解释5: 注意这一步,root不为null ,并且attachToRoot 为true时,才将从xml加载的View添加到root.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// 解释6:最后,如果root为null,或者attachToRoot为false,那么最终inflate返回的值就是从xml加载的View(temp),否则,返回的就是root(temp已添加到root)
if (root == null || !attachToRoot) {
result = temp;
}
}
}
...
//省略部分代码
return result;
}
从上面这段代码就能很清楚的说明前面提到的两种加载方式的区别了。
- 第一种加载方式 root为 null : 源码中的代码在 解释1 和 解释6 直接返回的就是从xml加载的temp View。
- 第二种加载方式 root不为null,attachToRoot为false: 源码中在 解释3 和解释5 ,为temp 设置了通过root生成的LayoutParams信息,但是没有add 添加到root 。
2.RecyclerView 部分源码分析
分析了inflate的源码,那么接下来我们就要看一下RecyclerView 的源码了,看一下是怎么加载item 到 RecyclerView 的。由于RecyclerView的代码比较多,我们就通过关键字来找,主要找holer.itemView
,加载的布局就是ViewHolder中的itemView.
通过源码我们找到了一个方法tryGetViewHolderForPositionByDeadline
,其中有一段代码如下:
//1,重点就在这里了,获取itemView 的LayoutParams
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
// 2,如果itemView获取到的LayoutParams为null,就生成默认的LayoutParams
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
其实重点就在这个方法里面了,看一下我注释的两个地方,先获取itemView的LayoutParams,如果获取到的LayoutPrams为null 的话,那么就生成默认的LayoutParams。我们看一下生成默认LayoutParams的方法generateDefaultLayoutParams
:
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
if (mLayout == null) {
throw new IllegalStateException("RecyclerView has no LayoutManager");
}
return mLayout.generateDefaultLayoutParams();
}
注意,里面又调用了mLayout的generateDefaultLayoutParams
方法,enerateDefaultLayoutParams
是一个抽象方法,具体的实现由对应的LayoutManager实现,我们用的是LinearLayoutManager,因此我们看一下LinearLayoutManager 的实现。
/**
* {@inheritDoc}
*/
@Override
public LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
卧槽,看到这儿大概就明白了item布局的宽高为什么会失效了,如果使用了默认生成LayoutParams这个方法,宽高都是WRAP_CONTENT。也就是说不管外面你的item根布局 宽高写的多少最终都是包裹内容。
其实按照前面的分析和前面的结果来看:
- 第一种加载方式(root为null):使用了这个方法,返回的view没有设置LayoutParams信息,所以在RecyclerView中使用的是默认的LayoutParams,导致我们设置的宽高失效。
- 第二种加载方式(root为parent,attachToRoot为false):使用了这个方法,返回的view设置了通过root生成的LayoutParams信息,所以在RecyclerView中使用的是我们设置的宽高。
总结
本文了解了infalte 加载布局的几种写法,也解释了每个参数的意义。最后通过源码解释了两种加载布局的方式在RecyclerView 中为什么一种宽高会失效,而另一种则不会失效。因此在使用RecyclerView写列表的时候,我们应该使用item布局不会失效的这种方式:
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item,parent,false)
如果加载布局时第三个参数设置为true呢?结果会一样吗?你会发现,一运行就会崩溃
?因为相当于 addView 了两次,RecyclerView中不应该这样使用。