Android 列表控件的Item设置布局宽高无效问题探究

邓琴文

本文从源码分析来解释一个开发中困扰了我许久的问题:给ListView的Item设置固定的高度无效,其他列表控件GridView和RecyclerView也有同样的问题。

我们通过代码复现一下问题,部分代码如下:

  • Activity代码:
private void initView() {  
      mListView = (ListView) findViewById(R.id.list_view);
        mList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            mList.add("这是第" + i + "个item");
        }
        mAdapter = new MyListAdapter(this,mList);
        mListView.setAdapter(mAdapter);
    } 
  • Adapter代码
public class MyListAdapter extends BaseAdapter {  
    private Context mContext;
    private List<String> mList;

    public MyListAdapter(Context context, List<String> list) {
        this.mContext = context;
        this.mList = list;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            viewHolder = new ViewHolder();
            convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
            viewHolder.tv = convertView.findViewById(R.id.tv);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.tv.setText(mList.get(position));
        return convertView;
    }

    static class ViewHolder {
        TextView tv;
    }
}
  • item布局文件如下:
<?xml version="1.0" encoding="utf-8"?>  
<LinearLayout  
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:gravity="center_vertical"
    android:background="#ffffff"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#ff00ff"
        android:gravity="center"/>
</LinearLayout>  

运行的结果如下: image

上面的情况我们加载布局的方式是:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

我们可以看到虽然我们给item的根布局设置了高度为100dp,但是并没有用item的高度还是TextView设置的50dp。

我们修改加载布局的方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent,false);

我们再次运行代码结果如下图: image

可以看到这个时候根布局设置的100dp生效了,textView为50dp,竖直居中显示。

所以加载布局的方式不同,会导致item设置宽高是否生效。那么这两种加载布局的写法到底有什么区别呢?我们通过源码分析来看一下inflate这个方法。

inflate 加载布局的方法

我们知道加载布局方式的不同会决定item设置的宽高是否有用,我们通过查看方法参数可以看到inflate有四种加载布局的方式。如下图: image

从上图可以看到 inflate 方法有四个重载方法,有两个方法第一个参数接收的是一个布局文件,另外两个接收的是XmlPullParse。

我们来看一下inflate接受布局文件ID的源码:

image

看源码就知道,接收布局文件的inflate方法里面调用的是接收XmlPullParse的方法。因此,我们一般只调用接收布局文件ID的inflate方法。两个重载方法的区别在于有无第三个参数attachToRoot, 而从源码里里面可以看到,两个参数的方法最终调用的是三个参数的inflate方法:

image

源码分析宽高失效原因

看过了inflate的几种方法,我们需要了解的就是三个参数的inflate方法,所以我们先去看一下三个参数的inflate方法参数是什么意思: image

  • parser:加载的布局文件资源id,如:R.layout.list_item。
  • root:如果attachToRoot(也就是第三个参数)为true, 那么root就是为新加载的View指定的父View。否则,root只是一个为返回View层级的根布局提供LayoutParams值的简单对象。
  • attachToRoot: 新加载的布局是否添加到root,如果为false,root参数仅仅用于为xml根布局创建正确的LayoutParams子类(列如:根布局为LinearLayout,则用LinearLayout.LayoutParam)。

接下来我们来分析一下三个参数的inflate 方法的源码,源码如下:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {  
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            //把传入的ViewGroup最终返回去了
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                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 { // 这里是重点
                    // Temp is the root view that was found in the xml
                   //首先创建了xml布局文件的根View——temp 
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;
                    //如果传入的root不为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);
                       //如果在root不为null, 并且attachToRoot为false,就为temp View(也就是通过inflate加载的根View)设置LayoutParams(root生成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");
                    }

                    // Inflate all children under temp against its context. 
                   //加载根布局View——temp和下面的子View(把tempView 添加到root)
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                  //如果root不为null ,并且attachToRoot 为true时,将从xml加载的View添加到root.(因为之前已经add了,这就是第二次add,会crash)
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                  // 最后,如果root为null,或者attachToRoot为false,那么最终inflate返回的值就是从xml加载的View(temp,没有设置LayoutParams),否则,返回的就是root(temp已添加到root)
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

这就是inflate的重要代码,从上面我们可以看出

  • 如果我们是
 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

这种加载布局的方法,我们没有指定新加载的View添加到哪个父容器,root为null,也没有root提供LayoutParams布局信息。这个时候直接返回的就是从xml加载的temp View。

      if (root == null || !attachToRoot) {
                        result = temp;
      }
  • 如果我们是
 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

这种加载布局的方式, root不为null ,attachToRoot 为false, 就为temp View(也就是通过inflate加载的根View)设置LayoutParams,并且通过rInflateChildren(parser, temp, attrs, true)加载了temp下面的子view。当然,如果加载布局时第三个参数设置为true时,一运行就会崩溃,因为相当于 addView 了两次,会crash。

那么inflate加载了布局之后ListView又是怎么把item布局加载进去的呢?

我们找到ListView里面的setupChild方法,这个方法的注释是将View添加到ViewGroup中,对子视图定位。

我们看一下核心的方法实现:

private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,  
            boolean selected, boolean isAttachedToWindow) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
       ... 省略部分代码
        //重点就是这里,获取子View的LayoutParams
        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
       //如果itemView获取到的LayoutParams为null,就生成默认的LayoutParams
        if (p == null) { 
            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
        }
        p.viewType = mAdapter.getItemViewType(position);
        p.isEnabled = mAdapter.isEnabled(position);
    }

通过上面可以看出,我们先获取itemView的LayoutParams,如果得到的LayoutParams为null,就使用默认的LayoutParams,而默认的LayoutParams,宽度是MATCHPARENT,高度是WRAPCONTENT。 image

到这里就知道为什么不同的布局加载方式会导致item设置宽高无效了。

第一种加载方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);

inflate的时候直接返回的就是从xml加载的temp View。没有设置LayoutParams信息,在添加到ListView时,得到的LayoutParams信息为null,所以设置了默认的LayoutParams信息,就是高度为WRAP_CONTENT,所以给item设置的固定高度没有用。

而第二种加载方式

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

inflate加载布局的时候返回的root View(添加了temp View的)。root View是有LayoutParams信息的。在添加到ListView时,得到的LayoutParams信息为root的LayoutParams信息,也就是item布局设置的宽高信息,所以给item设置的固定高度有用。

总结

通过查看源码我们了解了infalte 加载布局的几种写法,查看了ListView添加布局的方法,解释了两种加载布局的方式在ListView 中为什么一种宽高会失效,而另一种则不会失效。因此在使用列表控件写列表的时候,如果要设置item宽高有效,我们应该使用item布局不会失效的这种方式:

 convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);

其他列表控件GridView、RecyclerView也类似,如果感兴趣可以去看一下GridView和RecyclerView添加Item的方法,虽然有不同但是最终都是判断子View得到的LayoutParams信息是否为空,但是RecyclerView返回的默认LayoutParams信息是宽高都是WRAP_CONTENT。