【进阶android】ListView源码分析——适配器及观察者模式

来源:互联网 发布:网络病毒有哪些 编辑:程序博客网 时间:2024/05/16 04:41

     在常的编码习惯中,在初始化ListView实例,或者从布局文件之中引用了一个ListView实例之后,我们通常接着干的事儿,便是调用ListView的setAdapter方法,给当前ListView设置一个适配器。

      而在我们的印象中(未看源代码之前),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()两者的异同点。

    相同点:

  1. 两者都是采用数据观察者模式进行实现的;
  2. 两者都进行了数据的改变,将mDataChanged变量设置为true;
  3. 两者都更新了当前item数量,保存了上一次item的数量;
  4. 两者都检查了焦点,请求了重新布局;从而使得屏幕界面都将进行刷新。

     不同点:

       两者最主要的一个不同便是对同步信息的处理;notifyDataSetChanged()是需要保存同步信息的,及布局后要恢复布局前的同步信息;而notifyDataSetInvalidated()则是不必保存同步信息,布局后也无需恢复同步信息。举一个简单的例子,假设当前屏幕第一个视图对应的item的位置为5,那么调用notifyDataSetChanged()方法后,界面会使得屏幕第一个视图对应的item的位置恢复到5,而调用notifyDataSetInvalidated()方法之后,界面屏幕第一个视图对应的item的位置则将初始化为0。

0 0