Android中ListView用来处理View数组的显示,我们这里不介绍ListView的用法,而是介绍一下我在使用ListView时遇到的问题。对ListView的使用方法尚存疑问的同学,可以去搜索一下ListView的用法。
Header/Footer的添加和删除
在4.3(API-18)及以前的ListView中对Header的添加有限制,如果你在setAdapter(参数非空)之前没有调用addHeaderView,那么在setAdapter(参数非空)之后调用就会得到一个异常:
IllegalStateException Cannot add header view to list -- setAdapter has already been called."
让我们看一下该问题的原因。
首先看一下addHeaderView方法的源码:
public void addHeaderView(View v, Object data, boolean isSelectable) {
if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) {
throw new IllegalStateException(
"Cannot add header view to list -- setAdapter has already been called.");
}
//...
}
明了了,如果有Adapter,那么这个Adapter必须是HeaderViewListAdapter才能正常的添加Header。那么,这个HeaderViewListAdapter又是怎么来的呢?继续看源码会发现,这个HeaderViewListAdapter是在setAdapter中创建的:
public void setAdapter(ListAdapter adapter) {
//...
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
//...
}
很明显,创建HeaderViewListAdapter的前提是要有表头或者表尾数据。
相对的,添加Footer就没有这么多限制了,官方还特意在addFooterView方法上加了一段说明:
// NOTE: do not enforce the adapter being null here, since unlike in
// addHeaderView, it was never enforced here, and so existing apps are
// relying on being able to add a footer and then calling setAdapter to
// force creation of the HeaderViewListAdapter wrapper
在下学识浅薄不敢妄自翻译,敬请读者自己揣摩。
当一个ListView仅仅添加了Header和Footer(内容正在等待或没有),为了将Header和Footer显示出来,仍然要调用一下setAdapter,即使是使用null作为参数,也要执行一下,因为addHeaderView和addFooterView中,在添加了数据之后都有这样的代码:
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
幸运的是,Android4.4上已经对上述问题做了修改,现在的addHeaderView不再抛出异常了,让我们看一下源码:
public void addHeaderView(View v, Object data, boolean isSelectable) {
//...
// Wrap the adapter if it wasn't already wrapped.
if (mAdapter != null) {
if (!(mAdapter instanceof HeaderViewListAdapter)) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
}
// In the case of re-adding a header view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
也就是说,在Android4.4上可以随时添加Header而不用关心有没有调用setAdapter了,但是仍然需要调用setAdapter以便Header能够显示出来,因为mDataSetObserver是在setAdapter方法中创建的。
addFooterView方法也修改成和addHeaderView相似的了,之前版本上的那个好心的NOTE已经被删掉了。
不过,ListView对Header/Footer的支持到底有限,比如如果我想删除Header/Footer,我必须调用这两个方法:
public boolean removeHeaderView(View v)
public boolean removeFooterView(View v)
也就是说,我必须知道View的引用,那么如果想用这个功能,就必须自己维护一个Header/Footer的实时镜像引用列表,可谓复杂。如果有下面这样的方法,相信使用起来会方便的多。
public boolean removeHeaderView(int index)
public boolean removeFooterView(int index)
点击事件
点击事件包括单击事件和长按事件。
ListView有两个设置事件的方法:
public void android.widget.AdapterView.setOnItemClickListener(OnItemClickListener listener)
public void android.widget.AdapterView.setOnItemLongClickListener(OnItemLongClickListener listener)
这两个方法都是定义在AdapterView类中的,理所当然的,这个方法回调不会区分Header/footer以及ListAdapter中的View,所以当回调到OnItemClickListener的如下方法时
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
方法参数position指示的顺序是这样的:
Header:[0 ~ HeaderLength-1]
Content:[HeaderLength ~ HeaderLengh+AdapterLength-1]
Footer:[HeaderLength+AdapterLength ~ End]
这样的方法回调对于复杂的界面(包含可变数目的Header,Footer等)来说,不啻是个噩梦。可惜的是ListView没有提供如下这样的回调方法:
public void setOnHeaderItemClickListener(OnItemClickListener listener)
public void setOnHeaderItemLongClickListener(OnItemLongClickListener listener)
public void setOnFooterItemClickListener(OnItemClickListener listener)
public void setOnFooterItemLongClickListener(OnItemLongClickListener listener)
public void setOnContentItemClickListener(OnItemClickListener listener)
public void setOnContentItemLongClickListener(OnItemLongClickListener listener)
ListView 中,你不能单独设置某个项目支持LongClick或不支持,如果你调用了setOnItemLongClickListener,那么所有的项目都将会有长按事件回调,如果不想处理某些项目的长按事件,就必须自己写些代码了。
EmptyView
EmptyView,顾名思义,就是在ListView为空的时候显示的View。理论是,他可以是任何View,不过一般情况下,EmptyView和ListView在View树上是兄弟View。
但是,如果你在一个ListView中添加了Header和EmptyView,就会出现bug:当你设置的Adapter中没有内容的时候,EmptyView显示,但是Header却消失了。
跟进去查看EmptyView的显示逻辑是这样的:
updateEmptyStatus((adapter == null) || adapter.isEmpty());
private void updateEmptyStatus(boolean empty)
updateEmptyStatus接收一个boolean参数指示当前List是否为空,但是在adapter.isEmpty()上出了点差错,我们知道,带有Header的ListView的Adapter是HeaderViewListAdapter,然而看到该类的isEmpty方法是这么实现的:
public boolean isEmpty() {
return mAdapter == null || mAdapter.isEmpty();
}
这里的mAdapter是我们调用ListView.setAdapter设置的。
好了,原因出来了,因为这里没有考虑Header和Footer的数目,直接就给ListView判断成空的了!而EmptyView显示的时候AdapterView会把自己设置为GONE,于是,EmptyView和Header/Footer就不能共存了。