天天看点

Android ListView ClassCastException

前言

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

继续阅读