本文從源碼分析來解釋一個(gè)開發(fā)中困擾了我許久的問題:給ListView的Item設(shè)置固定的高度無效,其他列表控件GridView和RecyclerView也有同樣的問題。
我們通過代碼復(fù)現(xiàn)一下問題,部分代碼如下:
- Activity代碼:
private void initView() {
mListView = (ListView) findViewById(R.id.list_view);
mList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
mList.add("這是第" + i + "個(gè)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>
運(yùn)行的結(jié)果如下:
上面的情況我們加載布局的方式是:
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
我們可以看到雖然我們給item的根布局設(shè)置了高度為100dp,但是并沒有用item的高度還是TextView設(shè)置的50dp。
我們修改加載布局的方式:
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent,false);
我們再次運(yùn)行代碼結(jié)果如下圖:
可以看到這個(gè)時(shí)候根布局設(shè)置的100dp生效了,textView為50dp,豎直居中顯示。
所以加載布局的方式不同,會導(dǎo)致item設(shè)置寬高是否生效。那么這兩種加載布局的寫法到底有什么區(qū)別呢?我們通過源碼分析來看一下inflate這個(gè)方法。
inflate 加載布局的方法
我們知道加載布局方式的不同會決定item設(shè)置的寬高是否有用,我們通過查看方法參數(shù)可以看到inflate有四種加載布局的方式。如下圖:
從上圖可以看到 inflate 方法有四個(gè)重載方法,有兩個(gè)方法第一個(gè)參數(shù)接收的是一個(gè)布局文件,另外兩個(gè)接收的是XmlPullParse。
我們來看一下inflate接受布局文件ID的源碼:
看源碼就知道,接收布局文件的inflate方法里面調(diào)用的是接收XmlPullParse的方法。因此,我們一般只調(diào)用接收布局文件ID的inflate方法。兩個(gè)重載方法的區(qū)別在于有無第三個(gè)參數(shù)attachToRoot, 而從源碼里里面可以看到,兩個(gè)參數(shù)的方法最終調(diào)用的是三個(gè)參數(shù)的inflate方法:
源碼分析寬高失效原因
看過了inflate的幾種方法,我們需要了解的就是三個(gè)參數(shù)的inflate方法,所以我們先去看一下三個(gè)參數(shù)的inflate方法參數(shù)是什么意思:
- parser:加載的布局文件資源id,如:R.layout.list_item。
- root:如果attachToRoot(也就是第三個(gè)參數(shù))為true, 那么root就是為新加載的View指定的父View。否則,root只是一個(gè)為返回View層級的根布局提供LayoutParams值的簡單對象。
- attachToRoot: 新加載的布局是否添加到root,如果為false,root參數(shù)僅僅用于為xml根布局創(chuàng)建正確的LayoutParams子類(列如:根布局為LinearLayout,則用LinearLayout.LayoutParam)。
接下來我們來分析一下三個(gè)參數(shù)的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 { // 這里是重點(diǎn)
// Temp is the root view that was found in the xml
//首先創(chuàng)建了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)設(shè)置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時(shí),將從xml加載的View添加到root.(因?yàn)橹耙呀?jīng)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,沒有設(shè)置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添加到哪個(gè)父容器,root為null,也沒有root提供LayoutParams布局信息。這個(gè)時(shí)候直接返回的就是從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)設(shè)置LayoutParams,并且通過rInflateChildren(parser, temp, attrs, true)加載了temp下面的子view。當(dāng)然,如果加載布局時(shí)第三個(gè)參數(shù)設(shè)置為true時(shí),一運(yùn)行就會崩潰,因?yàn)橄喈?dāng)于 addView 了兩次,會crash。
那么inflate加載了布局之后ListView又是怎么把item布局加載進(jìn)去的呢?
我們找到ListView里面的setupChild方法,這個(gè)方法的注釋是將View添加到ViewGroup中,對子視圖定位。
我們看一下核心的方法實(shí)現(xiàn):
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
... 省略部分代碼
//重點(diǎn)就是這里,獲取子View的LayoutParams
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
//如果itemView獲取到的LayoutParams為null,就生成默認(rèn)的LayoutParams
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
}
通過上面可以看出,我們先獲取itemView的LayoutParams,如果得到的LayoutParams為null,就使用默認(rèn)的LayoutParams,而默認(rèn)的LayoutParams,寬度是MATCH_PARENT,高度是WRAP_CONTENT。
到這里就知道為什么不同的布局加載方式會導(dǎo)致item設(shè)置寬高無效了。
第一種加載方式:
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, null);
inflate的時(shí)候直接返回的就是從xml加載的temp View。沒有設(shè)置LayoutParams信息,在添加到ListView時(shí),得到的LayoutParams信息為null,所以設(shè)置了默認(rèn)的LayoutParams信息,就是高度為WRAP_CONTENT,所以給item設(shè)置的固定高度沒有用。
而第二種加載方式
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
inflate加載布局的時(shí)候返回的root View(添加了temp View的)。root View是有LayoutParams信息的。在添加到ListView時(shí),得到的LayoutParams信息為root的LayoutParams信息,也就是item布局設(shè)置的寬高信息,所以給item設(shè)置的固定高度有用。
總結(jié)
通過查看源碼我們了解了infalte 加載布局的幾種寫法,查看了ListView添加布局的方法,解釋了兩種加載布局的方式在ListView 中為什么一種寬高會失效,而另一種則不會失效。因此在使用列表控件寫列表的時(shí)候,如果要設(shè)置item寬高有效,我們應(yīng)該使用item布局不會失效的這種方式:
convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
其他列表控件GridView、RecyclerView也類似,如果感興趣可以去看一下GridView和RecyclerView添加Item的方法,雖然有不同但是最終都是判斷子View得到的LayoutParams信息是否為空,但是RecyclerView返回的默認(rèn)LayoutParams信息是寬高都是WRAP_CONTENT。