异常内容:
07-16 20:21:37.411: ERROR/AndroidRuntime(324): java.lang.IllegalStateException: The content of the adapter has changed but ListView did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(-1, class com.ringcentral.android.utils.ui.widget.PullToRefresh$7) with Adapter(class android.widget.HeaderViewListAdapter)]
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.widget.ListView.layoutChildren(ListView.java:1492)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.widget.AbsListView.onTouchModeChanged(AbsListView.java:1960)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.view.ViewTreeObserver.dispatchOnTouchModeChanged(ViewTreeObserver.java:591)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.view.ViewRoot.ensureTouchModeLocally(ViewRoot.java:2021)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.view.ViewRoot.ensureTouchMode(ViewRoot.java:2005)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.view.ViewRoot.handleMessage(ViewRoot.java:1774)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.os.Handler.dispatchMessage(Handler.java:99)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.os.Looper.loop(Looper.java:123)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at android.app.ActivityThread.main(ActivityThread.java:4627)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at java.lang.reflect.Method.invokeNative(Native Method)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at java.lang.reflect.Method.invoke(Method.java:521)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
07-16 20:21:37.411: ERROR/AndroidRuntime(324): at dalvik.system.NativeStart.main(Native Method)
怪就怪在报错的这个控件我的界面根本没用,而错误触发的时机却是在我的见面切换焦点所致~!(在模拟器上按上下左右键,就报错了。)。。。实在是无语。开始不想看源码,扭捏了一阵(主要是这错误太奇怪,不知从哪看起)。在界面里的相关控件中乱去设置setFocusable()。弄了一天,没有任何进展。哎,还是硬着头皮看源码吧。
根据异常堆栈信息,错误是报在ListView.layoutChildren()里的.
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
throw new IllegalStateException("The content of the adapter has changed but "
+ "ListView did not receive a notification. Make sure the content of "
+ "your adapter is not modified from a background thread, but only "
+ "from the UI thread. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
mItemCount就是ListView列表项的个数。是因为与adapter提供的count不一致,所以报错。
报错的信息是没错的,是我们工程中一个下拉刷新的控件写得有问题。由于问题不具有普遍性。我就着重讲一下这次研究源码所得到的收获.
ListView:
public void setAdapter(ListAdapter adapter) {
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
resetList();
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
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);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
int position;
if (mStackFromBottom) {
position = lookForSelectablePosition(mItemCount - 1, false);
} else {
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
if (mItemCount == 0) {
// Nothing selected
checkSelectionChanged();
}
} else {
mAreAllItemsSelectable = true;
checkFocus();
// Nothing selected
checkSelectionChanged();
}
requestLayout();
}
由于ListView需要先 addHeader再setAdapter, 而不能先setAdapter再addHeader。是因为下面这几行代码
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
原来ListView有header的时候,它会创建一个HeaderViewListAdapter的实例并将你的adapter作为参数传进去。因此,如果先设置了adapter,再想添加header,就无法添加了。
至于这里这个bug产生的原因,确切的操作步骤是先使用软键盘转换焦点,再用手指直接点击屏幕。这时Android就会产生一个事件表示TouchModeChanged,由于我们的UI结构是有一个父的Activity,然后有几个子界面是作为像Tab页那样显示在父界面中的,因此这几个子界面都会接到父界面派发的这个事件。这就解释为什么当前没有显示的界面也能收到这个消息并产生一系列动作。
当然,最主要的原因,还是这个下拉刷新的控件写得有问题(这个网上有现成的,不过我们不能用开源代码,所以是公司内部实现的),哪里有问题呢
listView = new ListView(context, attrs) {
@Override
public BaseAdapter getAdapter() {
if (adapter != null) {
return adapter.getInnerAdapter();
} else {
return null;
}
}
};
返回的是界面中实现的Adapter,为什么说问题在此?因此有header的ListView实际上用的是HeaderViewListAdapter,这样让ListView情何以堪啊。而且因为HeaderViewListAdapter会重写getCount等函数,会自动处理header和footer的数量,