这个问题很早之前就碰到过,后来通过 google 找到了解决办法,也就没有去管它了,直到最近有朋友问到这个问题,感觉很熟悉却又说不出具体原因,因此,就想通过源码分析一下。顺便做个总结,避免以后出现类似的问题。
为什么发现了这个问题呢?是当时要写一个列表,列表本来很简单,一行显示一个文本,实现起来也很容易,一个 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 的宽写成
? 那就去改一下嘛。进入 item 布局一看:
- wrap_content
不对啊,明明布局的宽写的是
, 为什么运行的结果就是包裹内容的呢?然后就想着既然 LinearLayout 作为根布局宽失效了,那就换其他几种布局方式试一下呢?
- match_parent
根布局换为 FrameLayout, 其他不变:
运行效果如下:
效果和 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 是加载布局的方式不同:
- LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, null)
以上这种加载方式 Item 宽高失效。
- LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, parent, false)
以上这种方式加载布局 item 不会出现宽高失效。,效果如下(宽和高都为 200dp):
问题我们算是定位到了,就是加载布局的方式不一样,那么这两种加载布局的写法到底有什么区别呢?这个我们就需要去深入了解
这个方法了
- inflate
上面我们定位到了 RecyclerView item 布局宽高失效的原因在于使用 inflate 加载布局时的问题,那么我们就看一下 inflate 这个方法:
从上图可以看到 inflate 方法有四个重载方法,有两个方法第一个参数接收的是一个布局文件 id,另外两个接收的是 XmlPullParse,看源码就知道,接收布局文件的 inflate 方法里面调用的是接收 XmlPullParse 的方法。
因此,我们一般只调用接收布局文件 ID 的 inflate 方法。两个重载方法的区别在于有无第三个参数
, 而从源码里里面可以看到,两个参数的方法最终调用的是三个参数的 inflate 方法:
- attachToRoot
第三个参数的值是根据第二个参数的值来判断的。
因此我们只需要分析一下三个参数的 inflate 方法,看一下这个方法的定义:
- /**
- * Inflate a new view hierarchy from the specified xml resource. Throws
- * {@link InflateException} if there is an error.
- *
- * @param resource ID for an XML layout resource to load (e.g.,
- * <code>R.layout.main_page</code>)
- * @param root Optional view to be the parent of the generated hierarchy (if
- * <em>attachToRoot</em> is true), or else simply an object that
- * provides a set of LayoutParams values for root of the returned
- * hierarchy (if <em>attachToRoot</em> is false.)
- * @param attachToRoot Whether the inflated hierarchy should be attached to
- * the root parameter? If false, root is only used to create the
- * correct subclass of LayoutParams for the root view in the XML.
- * @return The root View of the inflated hierarchy. If root was supplied and
- * attachToRoot is true, this is root; otherwise it is the root of
- * the inflated XML file.
- */
- public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
解释:从指定的 xml 资源文件加载一个新的 View,如果发生错误会抛出 InflateException 异常。
参数解释:
resource: 加载的布局文件资源 id, 如:R.layout.main_page。
root: 如果(也就是第三个参数) 为 true, 那么 root 就是为新加载的 View 指定的父 View。否则,root 只是一个为返回 View 层级的根布局提供 LayoutParams 值的简单对象。 attachToRoot: 新加载的布局是否添加到 root,如果为 false,root 参数仅仅用于为 xml 根布局创建正确的 LayoutParams 子类(列如:根布局为 LinearLayout,则用 LinearLayout.LayoutParam)。
- attachToRoot
了解了这几个参数的意义后,我们来看一下前面提到的两种写法
第一种:root 为 null
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, null)
这可能是我们用得比较多的一种方式,直接提供一个布局,返回一个 View, 根据上面的几个参数解释就知道,这种方式,没有指定新加载的 View 添加到哪个父容器,也没有 root 提供 LayoutParams 布局信息。这个时候,如果调用
返回的值为 null。通过上面的测试,我们知道这种方式会导致 RecyclerView Item 布局宽高失效。具体原因稍后再分析。
- view.getLayoutParams()
第二种:root 不为 null,attachToRoot 为 false
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, parent, false)
这种方式加载,root 不为 null,但是 attachToRoot 为 false,因此, 加载的 View 不会添加到 root, 但是会用 root 生成的 LayoutParams 信息。这种方式就是上面我们说的 RecyclerView Item 宽高不会失效的加载方式。
那么为什么第一种加载方式 RecyclerView Item 布局宽高会失效?而第二种加载方式宽高不会失效呢?我们接下来从原来来分析一下。
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 的代码比较多,我们就通过关键字来找,主要找
,加载的布局就是 ViewHolder 中的 itemView.
- holer.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
其实就是 RecyclerView 的布局管理器 LayoutManager.
- mLayout
可以看到
是一个抽象方法,具体的实现由对应的 LayoutManager 实现,我们用的是 LinearLayoutManager, 因此我们看一下 LinearLayoutManager 的实现。
- generateDefaultLayoutParams
- /**
- * {@inheritDoc}
- */
- @Override public LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- }
卧槽,看到这儿大概就明白了 item 布局的宽高为什么会失效了,如果使用了默认生成 LayoutParams 这个方法,宽高都是 WRAP_CONTENT。也就是说不管外面你的 item 根布局 宽高写的多少最终都是包裹内容。
那么前面说的两种方式哪一种用了这个方法呢?其实按照前面的分析和前面的结果来看,我们推测第一种加载方式(root 为 null)使用了这个方法,而第二种加载方式(root 不为 null,attachToRoot 为 false)则没有使用这个方法。因此我们断点调试看一下:
第一种加载方式:
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, null)
通过断点调试如上图,从 itemView 中获取的 layoutParams 为 null,因此会调用 generateDefaultLayoutParams 方法。因此会生成一个宽高都是 wrap_content 的 LayoutParams, 最后导致不管外面的 item 根布局设置的宽高是多少都会失效。
第二种加载方式:
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, parent, false)
断点调试如下图:
从上图可以看出,这种加载方式从 itemView 是可以获取 LayoutParams 的,为 RecyclerView 的 LayoutParams, 因此就不会生成默认的 LayoutParams,布局设置的宽高也就不会失效。
本文了解了 infalte 加载布局的几种写法,也解释了每个参数的意义。最后通过源码解释了两种加载布局的方式在 RecyclerView 中为什么一种宽高会失效,而另一种则不会失效。因此在使用 RecyclerView 写列表的时候,我们应该使用 item 布局不会失效的这种方式:
- View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.inflate_test_item, parent, false)
可能有的同学会问,如果加载布局时第三个参数设置为 true 呢?结果会一样吗?你会发现,一运行就会崩溃
为什么呢? 因为相当于 addView 了两次. RecyclerView 中不应该这样使用。
好了,以上就是全部内容,如有问题,欢迎指正。
更多精彩文章,尽在公众号【Android 技术杂货铺】
来源: http://blog.csdn.net/zwluoyuxi/article/details/76229425