而在我们的印象中(未看源代码之前),ListView的setAdapter无非是如下实现:
public void setAdapter(ListAdapter adapter) {
mAdapter = adapter;
}
很显然,此种想法是
不完善的!
在真正的ListView源码之中,setAdapter方法不仅仅只是设置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);//多选择模式下,清空所有被选择的item
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();//是否所有项目都可选择
mOldItemCount = mItemCount;//保存上一次的item数量
mItemCount = mAdapter.getCount();//保存当前item的数量
checkFocus();
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);//注册数据观察者
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());//item共有多少中视图类型
int position;
if (mStackFromBottom) {//从下到上
position = lookForSelectablePosition(mItemCount - 1, false);
} else {//从上到下
position = lookForSelectablePosition(0, true);
}
setSelectedPositionInt(position);//当前选择的item的位置
setNextSelectedPositionInt(position);//下次布局时,选择的item的位置
if (mItemCount == 0) {
// Nothing selected
checkSelectionChanged();
}
} else {
......
}
requestLayout();
}
整个ListView的setAdapter方法流程可用总结如下:
1、如果存在mAdapter及mDataSetObserver(AbsListView.AdapterDataSetObserver实例),则注销适配器对观察者的注册;
2、调用resetList()方法,重置列表:1)将所有的header或footer对应的视图的参数(recycledHeaderFooter)设置为false;2)将一些变量初始化为无效值;
3、调用mRecycler.clear()方法,清空二维数组废弃视图堆、临时视图数组中所有的废弃视图;mRecycler便是ListView的重用视图工具,后文会详细介绍;
4、如果存在页眉或者页脚视图,则将传进方法里的适配器封装为一个HeaderViewListAdapter实例,再将其赋值给当前ListView的适配器;
5、将上一次所选择的item的位置及行ID置空;
6、调用super.setAdapter(adapter)方法;AbsListView中的setAdapter方法只干了一件事,那就是如果当前ListView能够选择多个item(多选择模式),清空已经选择的所有item;
7、更新mItemCount、mOldItemCount两个变量;前者当前处理的item数量;后者保存前一次处理的item数量;
8、重新实例化一个AbsListView.AdapterDataSetObserver对象,并注册到方法入参传来的适配器之中;
9、设置item的视图类型;mRecycler会对每一种视图类型创建一个对应的废弃视图堆,用以为该种视图类型提供重用的视图;
10、找到当前被选择的item的位置和行ID,并将其赋值于本次布局被选择item的位置和下次布局被选择item的位置设置;
11、请求布局。
以上11个步骤中,第一、四、八步骤是本章的重点;同时会结合BaseAdapter类来分析适配器的notifyDataSetChanged()和notifyDataSetInvalidated()两个方法的实现流程;综上所述,笔者将分四方面来阐述:
1、适配器注册、注销观察者;
2、HeaderViewListAdapter类;
3、BaseAdapter中的notifyDataSetChanged()方法;
4、BaseAdapter中的notifyDataSetInvalidated()方法;
一、适配器注册、注销观察者;
适配器(BaseAdapter为例子)之中存在一个DataSetObservable的实例,DataSetObservable类相当于是一个元素类型为观察者(DataSetObserver)的ArrayList的封装,而封装的主要目的就是确保这个ArrayList是在一种线程安全的状态下,添加、删除元素。
观察者DataSetObserver是一个抽象类,该类代码如下:
public abstract class DataSetObserver {
/**
* This method is called when the entire data set has changed,
* most likely through a call to {@link Cursor#requery()} on a {@link Cursor}.
*/
public void onChanged() {
// Do nothing
}
/**
* This method is called when the entire data becomes invalid,
* most likely through a call to {@link Cursor#deactivate()} or {@link Cursor#close()} on a
* {@link Cursor}.
*/
public void onInvalidated() {
// Do nothing
}
}
二、HeaderViewListAdapter类;
HeaderViewListAdapter类,是对ListAdapter(BaseAdapter实现了该接口)的一种封装;而封装的主要目的是使得适配器能够像处理正常的item数据那样去处理页眉或者页脚,也就是说如果调用HeaderViewListAdapter类中的getCount方法,不止会返回所有item的数量,还会返回页眉、页脚数据的数量(也就是三种数据数量之和)。此外,该类还会提供一些专门针对页眉、页脚的方法,例如删除页眉(脚)、添加页眉(脚),获取页眉(脚)一种类型的总数量等。
与一般item数据相比,页眉(脚)数据是一种ListView.FixedViewInfo类型,FixedViewInfo类型将数据和视图绑定在了一起;而一般的item数据则是将数据与视图分离,只有在调用ListAdapter的getView方法才将数据和视图绑定在一起。如果是一般的item数据,则当调用HeaderViewListAdapter类的getView方法时,实际上调用的是ListAdapter类的getView。
三、BaseAdapter中的notifyDataSetChanged()方法;
前两点主要讲述有关的都和适配器有关,第三、四点则侧重与观察者模式;在重点讲述ListView的观察者流程之前;先要明确一点,那就是什么是观察者模式;所谓观察者模式,即一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。将这个概念拿到ListView之中,进一步而言,拿到BaseAdapter之中,BaseAdapter是被观察者,DataSetObserver则是观察者,当BaseAdapter发觉到有数据变化时,则将通知所有注册于适配器之中的观察者。因此,适配器提供了两个方法用以主动通知所有的观察者。当使用适配器时,如果发生数据改变时,应该主动调用这两个方法。
言归正传,这两个方法分别为notifyDataSetChanged()和notifyDataSetInvalidated();我们先看notifyDataSetChanged()。
BaseAdapter中的notifyDataSetChanged()代码如下:
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
根据第一点所示,mDataSetObservable乃DataSetObservable的一个对象。DataSetObservable中的notifyChanged方法则主要依次调用注册于其中DataSetObserver对象的onChange方法。DataSetObserver是一个抽象类,其中主要定义了两个什么都没有做的方法:onChanged()和onInvalidated();BaseAdapter,或者直接说ListView之中并未直接使用DataSetObserver,而是使用它的一个直接子类AdapterView.AdapterDataSetObserver;
此时,我们可用进行一个总结,BaseAdapter的notifyDataSetChanged()方法的处理本质,DataSetObservable类中的onChanged()方法;而在ListView中,DataSetObservable对象实际上是AdapterDataSetObserver对象;因此,下面我们重点看看AdapterDataSetObserver的onChanged()方法。
AdapterDataSetObserver的onChanged()方法源码如下:
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect(检测) the case where a cursor that was previously
//(之前) invalidated(作废) has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
/*当调用adpter中的notifyDataSetChanged方法后,
*adpterView视图(例如ListView)的屏幕会恢复到
*调用notifyDataSetChanged方法之前的位置;其原
*因就是在重回界面之前调用rememberSyncState方法
*记录被选则项目的位置*/
rememberSyncState();
}
checkFocus();
requestLayout();
}
这个方法之中可以分为四个步骤:
1、将mDataChanged设置为true,标识此时的ListView数据已经改变;
2、更新item的数量,并记录上一次的item数量;
3、调用rememberSyncState()方法,保存当前同步状态,确保布局成功后能够恢复相应的状态;
4、检测焦点,请求布局。
四个步骤中,1、2、4此刻暂不细讲,直接进入remeberSyncState()方法,源码如下:
void rememberSyncState() {
if (getChildCount() > 0) {
mNeedSync = true;
mSyncHeight = mLayoutHeight;
if (mSelectedPosition >= 0) {//存在被选择的item
// Sync the selection state
View v = getChildAt(mSelectedPosition - mFirstPosition);//获取对应的视图
mSyncRowId = mNextSelectedRowId;
mSyncPosition = mNextSelectedPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_SELECTED_POSITION;
} else {
// Sync the based on the offset of the first view
View v = getChildAt(0);
T adapter = getAdapter();
if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
mSyncRowId = adapter.getItemId(mFirstPosition);
} else {
mSyncRowId = NO_ID;
}
mSyncPosition = mFirstPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_FIRST_POSITION;
}
}
}
该方法的主要目的是保存相关用以同步的变量(mSyncHeight、mSyncRowId、mSyncPosition及mSyncMode),从而确保在布局之后,能够通过这些同步变量的值来恢复相应的屏幕状态(例如屏幕位置、视图滚动量等)。
首先细说一下这四个同步变量的意义;
- mSyncHeight:同步时视图的高度;
- mSyncPosition:同步时,从哪个位置开始寻找具有mSyncRowId的item;
- mSyncRowId:进行同步时,从哪个item开始同步;
- mSyncMode:同步模式;一共有两种同步模式:SYNC_SELECTED_POSITION和SYNC_FIRST_POSITION;SYNC_SELECTED_POSITION表示同步的item是当前被选择的item;SYNC_FIRST_POSITION表示同步的item是当前展示屏幕中第一个子视图对应的item。
该方法之中,首先将mNeedSync设置为true表示布局之后需要同步,而后更新mSyncHeight,接着根据是否存在被选择的item来确定同步模式;若是SYNC_SELECTED_POSITION模式则将mNextSelectedRowId与mNextSelectedPosition分别设置给mSyncRowId及mSyncPosition;若是SYNC_FIRST_POSITION模式则将当前屏幕中的第一个视图对应的item的行ID及位置设置给mSyncRowId及mSyncPosition。
在ListView中mNextSelectedRowId与mNextSelectedPosition表示布局之后的选择item的行ID及位置。
四、BaseAdapter中的notifyDataSetInvalidated()方法;
与BaseAdapter类中的notifyDataSetChanged()方法类似,notifyDataSetInvalidated()方法实质是调用的DataSetObservable类中的onInvalidated()方法;而在ListView中,DataSetObservable对象实际上是AdapterDataSetObserver对象;因此我们直接分析AdapterDataSetObserver中的onInvalidated()方法。
源代码如下:
public void onInvalidated() {
mDataChanged = true;
if (AdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = AdapterView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkFocus();
requestLayout();
}
该方法主要的目的是将数据集设置为无效,进一步而言是将当前选择item及布局后的选择item设置为无效,并且不进行相应的同步。
最后,我们来对比一下notifyDataSetChanged()和notifyDataSetInvalidated()两者的异同点。
相同点:
- 两者都是采用数据观察者模式进行实现的;
- 两者都进行了数据的改变,将mDataChanged变量设置为true;
- 两者都更新了当前item数量,保存了上一次item的数量;
- 两者都检查了焦点,请求了重新布局;从而使得屏幕界面都将进行刷新。
不同点:
两者最主要的一个不同便是对同步信息的处理;notifyDataSetChanged()是需要保存同步信息的,及布局后要恢复布局前的同步信息;而notifyDataSetInvalidated()则是不必保存同步信息,布局后也无需恢复同步信息。举一个简单的例子,假设当前屏幕第一个视图对应的item的位置为5,那么调用notifyDataSetChanged()方法后,界面会使得屏幕第一个视图对应的item的位置恢复到5,而调用notifyDataSetInvalidated()方法之后,界面屏幕第一个视图对应的item的位置则将初始化为0。