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、数据源通知更新。

ListView解析的更多相关文章

  1. 20150226—C# winform中的ListView解析

    ListView在WinForm中多用于表的构建,可以直观的显示表的信息,其格式如同SQL的表 这是他的位置,在公共控件中: Listview的几个重要属性:Columms(集合).Groups(集合 ...

  2. S1293和S2220KTV项目结束

    1.界面原型(前台的界面搭建一下) 2.数据库 3.架构设计 4.约定的文件抽取 2015年7月20日下午 歌星点歌三界面的联动,数据动态加载 01.点击第一个LIstView,弹出第二个ListVi ...

  3. S1的小成果:MyKTV系统

    转眼之间,已经到了2016年,即新的一年了!S1也结束了,收获的也不多 ,想想最后留给大家的就一个KTV项目了. 希望大家看时有所收获           现在我们一起来看KTV前台管理 主界面的运行 ...

  4. 织梦后台如何生成站点地图sitemap.xml

    第一步在网站根目录建立sitemap.php文件 内容如下: 写一个计划任务文件命名为generate_sitemap.php,放在/plus/task目录里,文件内容如下: <?php//定时 ...

  5. 解析ListView联动的实现--仿饿了么点餐界面

    一.博客的由来 大神王丰蛋哥 之前一篇博客仿饿了点餐界面2个ListView联动(http://www.cnblogs.com/wangfengdange/p/5886064.html) 主要实现了2 ...

  6. 安卓解析json,使用BaseAdapter添加至ListView中,中间存储用JavaBean来实现

    这是一个小练习,要求解析一个提供的json文件.并将其中的id,title值获取,以ListView形式展示出来.(开发工具是android studio) 下面开始: 首先我想到的是先把json文件 ...

  7. Json文件放入Assets文件,读取解析并且放入listview中显示。

    package com.lixu.TestJson; import android.app.Activity; import android.content.Context; import andro ...

  8. 一步步教你为网站开发Android客户端---HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新ListView

    本文面向Android初级开发者,有一定的Java和Android知识即可. 文章覆盖知识点:HttpWatch抓包,HttpClient模拟POST请求,Jsoup解析HTML代码,动态更新List ...

  9. json解析,异步下载(listview仅滑动时加载)Demo总结

    异步加载的练习demo 主要涉及知识点: 1.解析json格式数据,主要包括图片,文本 2.使用AsynTask异步方式从网络下载图片 3.BaseAdapter的"优雅"使用 4 ...

随机推荐

  1. Kubernets二进制安装(8)之部署四层反向代理

    四层反向代理集群规划 主机名 角色 IP地址 mfyxw10.mfyxw.com 4层负载均衡(主) 192.168.80.10 mfyxw20.mfyxw.com 4层负载均衡(从) 192.168 ...

  2. (20002, b'DB-Lib error message 20002, severity 9:\nAdaptive Server connection failed (127.0.0.1:3306)\n')

    使用python 3.7 pymssql 连接本地mysql 5.6 报错 解决:参考 https://www.cnblogs.com/springbrotherhpu/p/11503139.html ...

  3. Leetcode(94)-二叉树的中序遍历

    给定一个二叉树,返回它的中序 遍历. 示例: 输入: [1,null,2,3] 1 \ 2 / 3 输出: [1,3,2] 思路:和上篇的前序遍历一样,同样有递归和非递归的做法 (1)递归 vecto ...

  4. 调用其他文件__name__=='__main__'下代码

    如何调用其他文件__name__=='__main__'下代码 使用os.system()或者subprocess.run()执行该文件,用这种方法相当于直接创建了一个子进程,新调用的py不影响当前调 ...

  5. C++ 0LL

    C++ 0LL C plus plus L / l means long LL === long long int countDigitOne(int n) { int countr = 0; for ...

  6. how to close macos eject icon from menu bar

    how to close macOS eject icon from the menu bar close eject https://apple.stackexchange.com/question ...

  7. NGK公链生态所如何保障用户的数字资产隐私安全?

    距离NGK生态所正式上线已经没剩下几天时间了,NGK全网算力总量正在持续猛增,NGK日活账户也在大幅度增多.可以看出,币圈的生态建设者们是十分看好NGK生态所的.那么,有这么多的生态建设者涌入NGK生 ...

  8. 通过setMouseTracking实现用鼠标拖动控件

    1 import sys 2 from PyQt5.Qt import * 3 4 class Mwindow(QWidget): 5 leftclick = False 6 7 def __init ...

  9. 调用Config.ini类

    private static string sPath = @Directory.GetCurrentDirectory() + "\\config.ini"; [DllImpor ...

  10. close() 和fluse()区别

    1.close()默认包含了一次flush()操作,关闭之后,就不能再写入了. 2.flush()刷新,flush()之后,可以接着写入. 3.缓冲区默认大小是8192字节,如果小于8192字节,不会 ...