ListView解析

 

ListView通过一个Adapter来完成数据和组件的绑定。以ListActivity为例,它集成自Activity,里面包含有一个ListAdapter和一个ListView。绑定的操作通过setListAdapter来完成。本文主要通过源码,来说明,具体的绑定过程究竟是如何进行的,以及convertView(Adapter的getView的第二个参数)缓存实现机制。

如下是ListActivity的代码片段:

    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected ListAdapter mAdapter;
    /**
     * This field should be made private, so it is hidden from the SDK.
     * {@hide}
     */
    protected ListView mList;

最关键的绑定动作由下:

    public void setListAdapter(ListAdapter adapter) {
        synchronized (this) {
            ensureList();
            mAdapter = adapter;
            mList.setAdapter(adapter);
        }
    }

其中调用了ListView的setAdapter完成,传入的参数类型为ListAdapter。

    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();
    ...

由上述代码,resetList主要是用来重置ListView的headerView和footView的。关键的一个方法是mRecycler.clear(),成员变量mRecycler是从AbsListView继承而来的,它的类型是RecycleBin,从名字看起来好像和“回收”有关。查看源码,有这样一段描述RecycleBin的话:

     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
     * start of a layout. By construction, they are displaying current information. At the end of
     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
     * could potentially be used by the adapter to avoid allocating views unnecessarily.

总而言之,RcycleBin是用来缓存View,以避免不必要的回收View的——设想一下,如果向下滚动ListView后,再回滚到原来位置,如果要重新把View都生成一遍,那要消耗一定的时间。如果缓存起来,对View直接填充数据即可,这也是现在通用的办法。

    class RecycleBin {
        private RecyclerListener mRecyclerListener;

        /**
         * The position of the first view stored in mActiveViews.
         */
        private int mFirstActivePosition;

        /**
         * Views that were on screen at the start of layout. This array is populated at the start of
         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
         * Views in mActiveViews represent a contiguous range of Views, with position of the first
         * view store in mFirstActivePosition.
         */
        private View[] mActiveViews = new View[0];

        /**
         * Unsorted views that can be used by the adapter as a convert view.
         */
        private ArrayList<View>[] mScrapViews;

        private int mViewTypeCount; //代表需要显示的View的类型的个数:ListView中不是所有的View的类型都一样,不过在BaseAdapter里,默认是1

        private ArrayList<View> mCurrentScrap; //表示当前类型的scrap的View

  根据解释,mFirstActivePosition是存储在mActiveViews的第一个View的position。什么意思?待会解释。其中mActiveViews是一个View数组,保存的是当前屏幕可见的所有View。不可见的View都“移动到”mScrapViews(scrap的含义是废弃的,因此mScrapViews保存的应该是所有“废弃”的View)——把不可见的View都当做废弃的View保存起来,并没有直接释放,这就是缓存。在这里可以解释一下mFirstActivePosition的含义:比如现在屏幕上显示的是3,4,5三个View,则mFirstActivePosition为3,即第一个处于Active状态的View是第三个。

  保存“废弃”的View的mScrapViews是ArrayList<View>[]类型,即它是一个数组,每个数组保存的是View的链表。不像mActiveViews,mScrapViews是可以动态改变的,结合实际情况,每个屏幕可以显示的View的数量是一定的,但是不可见的View可就太多太多了,所以这符合实际需求。其中“Unsorted views that can be used by the adapter as a convert view”,表明,convertView就是mScrapViews中的某个View。

  在setAdapter里面调用了mRecycler.clear(),下面来看看这个方法:

        /**
         * Clears the scrap heap.
         */
        void clear() {
            if (mViewTypeCount == 1) {
                final ArrayList<View> scrap = mCurrentScrap;
                final int scrapCount = scrap.size();
                for (int i = 0; i < scrapCount; i++) {
                    removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
                }
            } else {
                final int typeCount = mViewTypeCount;
                for (int i = 0; i < typeCount; i++) {
                    final ArrayList<View> scrap = mScrapViews[i];
                    final int scrapCount = scrap.size();
                    for (int j = 0; j < scrapCount; j++) {
                        removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
                    }
                }
            }
        }

  从clear()方法中可以看到,如果mViewTypeCount == 1,则只需清楚mCurrentScrap的内容即可;否则按照不同的类型,都统统清除掉。这里的清除是调用ViewGroup的removeDetachedView将View从View树中去掉。

  分析到这里,还是在setListAdapter方法里面,它首先作了清除View的动作。

 

    public void setAdapter(ListAdapter adapter) {
        if (mAdapter != null && mDataSetObserver != null) {
            mAdapter.unregisterDataSetObserver(mDataSetObserver);
        }

        resetList();
        mRecycler.clear();
        ...

        // AbsListView#setAdapter will update choice mode states.
        super.setAdapter(adapter);

        if (mAdapter != null) {
            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
            mOldItemCount = mItemCount;
            mItemCount = mAdapter.getCount(); //获取Adapter的getCount值,这个值是根据数据源的个数来设定的
            checkFocus();

            mDataSetObserver = new AdapterDataSetObserver();
            mAdapter.registerDataSetObserver(mDataSetObserver); //注册数据集观察者,待数据源大小有变时,需要更新ListView

            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); //默认是1(BaseAdapter里实现)
            ...
        } else {
            mAreAllItemsSelectable = true;
            checkFocus();
            // Nothing selected
            checkSelectionChanged();
        }

        requestLayout(); //布局
    }

  分析到这里,View就加载完成了。下面分析数据源有变化时,如何利用NotifyDataSetChanged来更新View。

    public void notifyDataSetChanged() {
        mDataSetObservable.notifyChanged();
    }

  实际调用的是DataSetObservable的方法notifyChanged(),经过多次调用,最终调用到的是AdapterView的内部类AdapterDataSetObserver的onChanged——重写了DataSetObserver:

    class AdapterDataSetObserver extends DataSetObserver {

        private Parcelable mInstanceState = null;

        @Override
        public void onChanged() {
            mDataChanged = true;
            mOldItemCount = mItemCount;
            mItemCount = getAdapter().getCount(); //再次获取到View的个数

            // 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 {
                rememberSyncState();
            }
            checkFocus();
            requestLayout(); //布局
        }

 

  在ListView滚动时,只需要通知数据源发生了变化即可自动更新View。在ListView中有一个makeAndAddView方法,该方法根据需要重新生成一个View,或者使用(reuse)缓存起来的View。ListView通过AbsListView的obtainView调用getView——这个getView就是我们需要重载的那个getView。

    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
            boolean selected) {
        View child;


        if (!mDataChanged) {
            // Try to use an existing view for this position
            child = mRecycler.getActiveView(position); //判断特定position的View是否存在,如果存在,则选出来。
            if (child != null) {
                if (ViewDebug.TRACE_RECYCLER) {
                    ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
                            position, getChildCount());
                }

                // Found it -- we're using an existing child
                // This just needs to be positioned
                setupChild(child, position, y, flow, childrenLeft, selected, true);

                return child;
            }
        }

        // Make a new view for this position, or convert an unused view if possible
        child = obtainView(position, mIsScrap);

        // This needs to be positioned and measured
        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);

        return child;
    }

 

至此,基本上就分析完了。

小结:

  1、可以加深为什么在继承BaseAdapter时,需要重写几个方法那几个方法。

  2、数据源通知更新。

 

posted @ 2015-03-30 21:03  徐小鱼  阅读(385)  评论(0编辑  收藏  举报