本文从源码分析来解释一个开发中困扰了我许久的问题:给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 mList;
public MyListAdapter(Context context, List 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布局文件如下:
运行的结果如下:
上面的情况我们加载布局的方式是:
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);
我们再次运行代码结果如下图:
可以看到这个时候根布局设置的100dp生效了,textView为50dp,竖直居中显示。
所以加载布局的方式不同,会导致item设置宽高是否生效。那么这两种加载布局的写法到底有什么区别呢?我们通过源码分析来看一下inflate这个方法。
inflate 加载布局的方法
我们知道加载布局方式的不同会决定item设置的宽高是否有用,我们通过查看方法参数可以看到inflate有四种加载布局的方式。如下图:
从上图可以看到 inflate 方法有四个重载方法,有两个方法第一个参数接收的是一个布局文件,另外两个接收的是XmlPullParse。
我们来看一下inflate接受布局文件ID的源码:
看源码就知道,接收布局文件的inflate方法里面调用的是接收XmlPullParse的方法。因此,我们一般只调用接收布局文件ID的inflate方法。两个重载方法的区别在于有无第三个参数attachToRoot, 而从源码里里面可以看到,两个参数的方法最终调用的是三个参数的inflate方法:
源码分析宽高失效原因
看过了inflate的几种方法,我们需要了解的就是三个参数的inflate方法,所以我们先去看一下三个参数的inflate方法参数是什么意思:
- 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(" 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,宽度是MATCH_PARENT,高度是WRAP_CONTENT。
到这里就知道为什么不同的布局加载方式会导致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。