前言
8012年了,在Android開發中,還是避免不了使用 ListView,通過
addFooterView
去添加底部視圖,UI重新整理時,又通過
removeFooterView
去移除舊的視圖,在Android 4.3版本及以下
removeFooterView
時,發生閃退,日志如下:
com.company.adapters.MyAdapter cannot be cast to android.widget.HeaderViewListAdapter
at android.widget.ListView.removeFooterView(ListView.java:)
問題原因
檢視源碼,發現了出問題的代碼:
【注:本文所有源碼都是 ListView.java,隻是版本不同】
//Android 4.2版本
public boolean removeFooterView(View v) {
if (mFooterViewInfos.size() > ) {
boolean result = false;
if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
result = true;
}
removeFixedViewInfo(v, mFooterViewInfos);
return result;
}
return false;
}
直接強轉 mAdapter 為
HeaderViewListAdapter
,顯然這裡是異常的根源,此時的 mAdapter 根本不是
HeaderViewListAdapter
類型。
那麼為什麼 4.4及以上是可以的呢,我們對比代碼,發現4.3及以下的
addFooterView
和4.4及以上的有些許不同:
Android 4.3及以下版本
//Android 4.3版本
public void addFooterView(View v, Object data, boolean isSelectable) {
FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
if (mAdapter != null && mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
Android 4.4及以上版本
//Android 4.4版本
public void addFooterView(View v, Object data, boolean isSelectable) {
final FixedViewInfo info = new FixedViewInfo();
info.view = v;
info.data = data;
info.isSelectable = isSelectable;
mFooterViewInfos.add(info);
// 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 footer view, or adding one later on,
// we need to notify the observer.
if (mDataSetObserver != null) {
mDataSetObserver.onChanged();
}
}
}
可以看到,4.4版本判斷如果 mAdapter 不為空,則在 mAdapter 外包一層
HeaderViewListAdapter
,這樣 mAdapter 的類型永遠都是
HeaderViewListAdapter
,是以不會出現
ClassCastException
。
解決方案
在研究過 ListView 代碼後,我們發現,在 Android 4.2 的
addFooterView
的注釋上,有這麼一句:“在調用 setAdapter 之前先調用 addFooterView ”
/**
* NOTE: Call this before calling setAdapter. This is so ListView can wrap
* the supplied cursor with one that will also account for header and footer
* views.
*/
public void addFooterView(View v, Object data, boolean isSelectable) { //...}
為什麼呢?我們繼續看看在
setAdapter
方法中怎麼寫的:
@Override
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
// size大于0,說明有 headerView/footerView
if (mHeaderViewInfos.size() > || mFooterViewInfos.size() > ) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
//...略
}
原來在
setAdapter
中,如果發現 ListView 中有 HeaderView/FooterView ,則将 mAdapter 包一層
HeaderViewListAdapter
,也就是說,如果我們在
setAdapter
之前就調用 addFooterView/addHeaderView ,再
setAdapter
,這樣就不會報錯了。
先調用 addFooterView ,會執行
mFooterViewInfos.add(info);
将 footerView 的相關資訊加進去,這樣size>0,後面再 setAdapter 時,就會包一層
HeaderViewListAdapter
,不會出現類型轉換異常。
結論
在Android 4.3及以下版本,在使用 ListView 時,如果要添加 headerView/FooterView ,順序一定是:
mListView.addFooterView(footerView);
mListView.setAdapter(mAdapter);
mListView.removeFooterView(footerView);
這樣就能相容 4.3及以下版本。
如果隻針對Android 4.4 以上版本,則無需考慮順序。總的來說,先 addFooterView 再 setAdapter 最保險。
參考資料
https://stackoverflow.com/questions/24700588/i-am-using-the-listview-add-remove-footer-for-listview-cross-app-in-android-vers