1.观察者模式和模板模式
因为我们可使用adapter. notifyDataSetChanged()来通知ListView进行数据刷新,所以可以初步猜测BaseAdapter类是被观察者类,ListView是观察者类,所以BaseAdapter里应该会有个存储观察者的集合:
其中,DataSetObservable就是封装了该集合,在BaseAdapter中确定该集合装的元素的类型是DataSetObserver
可见,该集合是抽象类Observable的成员变量
而DataSetObservable是Observable的子类,因DataSetObservable是在BaseAdapter 中进行实例化的,所以DataSetObservable设置Observable的泛型类就是BaseAdapter的registerDataSetObserver方法中传入的DataSetObserver类型。
现在推测ListView应该是继承或实现了DataSetObserver的。现在进行论证,在ListView的setAapter方法中:
可知这里注册了观察者,其中mAdapter就是setAapter方法传进来的Adapter。这个观察者是AdapterDataSetObserver,经查询,AdapterDataSetObserver不是ListView里面的类,那么可猜测,它应该是ListView的父类里面的类,确定一下:
它确实是ListView的父类AbsListView里的类,它在onChanged方法里调用super.onChanged()方法,即AdapterView的AdapterDataSetObserver(继承自DataSetObserver)的onChaged()方法,在该方法里调用requestLayout()方法从而实现刷新页面,如下图:
可知,AbsListView与AdapterView并没有继承DataSetObserver或AdapterDataSetObserver,而是在它们里面分别单独写了个内部类AdapterDataSetObserver作为观察者。
查看BaseAdapter里notifyDataSetChanged方法:
调用了DataSetObservable的notifyChanged方法:
可知,变量所有观察者,然后调用观察者的onChanged()方法。
2.ListView显示第一屏
先分析onLayout方法,该方法在ListView的父类AbsListView中:
调用了layoutChildren()对itemView进行布局,查看layoutChildren():
因为layoutChildren()在AbsListView中没有实现,而且是被protected修饰的,可知该方法具体应该是在ListView重写的,因为AbsListView的子类有多个,比如ListView和GridView等,这些子类的item的摆放方式各不相同,所以应该由子类具体来实现layoutChildren方法(模板模式),看ListView的layoutChildren方法:
重点看这个default中:
绘制第一屏的第一个item时,childCount肯定为0,其中1655行的fillFromTop方法用于从上往下填充item。
调用fillDown方法:
当前Itme的高度小于end且当前的positon小于item的个数,就在这个循环里填充一屏。
看makeAndAddView方法:
mRecycler就是回收池,第一次的时候,从回收池拿到的child肯定为null的,所以走到1840行child=obtainView(position,mIsScan), obtainView方法是在AbsListView中的:
mRecycler是回收池,当然绘制第一屏的时候,从回收池得到的都为null
所以第一屏绘制就会跑到2177行,就调用了适配器的getView方法。
看setupChild方法:
填充第一屏的时候,setupChild方法的最后一个参数recycled的布尔值肯定为false。
所以进入这个else里面,在第1888行调用addViewInLayout方法,该方法是ViewGroup里的,所以此时child就添加到ListView了。
3.回收池
因为上述涉及到了回收池,这里先捋顺一下回收池相关的逻辑:
mRecycler是AbsListView的成员对象"final RecycleBin mRecycler = new RecycleBin()",RecycleBin是AbsListView的内部类:
setViewTypeCount方法里对mScrapView和mCurrentScrap进行赋值,可知轻松得知:
mScrapView数组的元素index是itemView的type,元素的值是该type的itemView的集合。mCurrentScrap是mScrapView存的第一个type对应的itemView的集合。
这里分析将View添加到缓存池和从缓存池取出View的方法。
先看将View添加到缓存池,即RecycleBin的addScrapView(View scrap, int position)方法,重点是
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews [viewType].add(scrap);
}
即根据mViewTypeCount的数量来确定存在mCurrentScrap还是mScrapViews:如果mViewTypeCount是1,即itemView的类型只有一种,那么要缓存的View scrap就存放在mCurrentScrap中,否则根据viewType的值来绝对存放在mScrapViews对应的数组中。
再看从缓存池取出View的方法:
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
也同样是先调用适配器的getItemViewType(position)得到当前position对应的itemViewType,如果当前列表的itemViewType的数量mViewTypeCount为1,就通过retrieveFromScrap(mCurrentScrap, position)从mCurrentScrap的尾部取出一个itemView并且返回,否则通过retrieveFromScrap(mScrapViews[whichScrap], position)从mScrapViews[whichScrap]的尾部取出一个itemView并且返回。
注意,一个itemView添加到回收池之前要将它与它的父节点的关系断开,否则会出现问题,如:
通过new出来的View是可以添加到ListView中的,而如下的TextView与ListView的父容器是同一个,则不能addView添加的该ListView中:
通过findViewById得到该TextView,是不能addView添加到该ListView中的。因为View的结构是树形结构(如下),即一个View只能有一个父容器,如果上图的TextView通过addView添加到上图的ListView中,那么该TextView就有两个父容器了,这是不符合树型原则的。
所以,一个View要能添加到回收池的条件就是该View没有父容器,也就是添加到回收池之前要把该View的父容器置为空,所以在addScrapView方法中:
调用了View的dispatchStartTemporaryDetch()方法,这个方法就是把调用该方法的View的父节点解耦置空了。
4.第一屏的刷新
接下来研究非显示第一屏的情况,先分析第一屏数据加载后用户调用notifyDataChanged方法后的表现:
首先调用notifyDataSetChanged()方法后,就会调用onLayout然后调用layoutChildren方法,在layoutChildren方法中,符合上图的if(dataChanged)判断,所以通过for循环把所有的child添加到回收池中。
在layoutChildren方法中,因为我们现在要研究的是非第一屏显示的View,所以进入childCount不等于0的判断中,第1662是当前选择到的position进行判断,当然如果我们没有选择一个item,就进入1666行,看fillSpecific方法:
第1321行调用makeAndAddView方法,传入该方法的第三个参数是true:
通过回收池得到一个活动的child,然后调用setupChild,注意第四个参数为true:
即flowDown为true
因为此时recycled为true,所以调用attachViewToParent方法,该方法与addViewInLayout的区别:后者是把一个全新的View添加到ViewGroup,前者只是把view与它的parent重新确立关系(因为添加到回收池的时候解除了和父容器的关系),所以这前者性能比后者大很多。所以加载第一屏的时候的速度远比加载第二屏及以后屏的的速度慢。刚才分析的是第一屏数据加载后用户调用notifyDataSetChanged()方法后的表现,还没有分析用户滑动屏幕后的表现。这样通过notifyDataSetChanged()方法调用后就把刷新了页面了。
5、滑动显示新页
当用户往上滑动屏幕,第一个item慢慢滑出屏幕,然后底部慢慢显示完整的一个item,这个是怎么实现的呢:
看onTouchEvent,它在AbsListView中重写了,在它的MOVE事件里:
会走到TOUCH_MODE_SCROLL这个case,所以进入scrollIfNeed(y),其中y是手指的坐标:
通过incrementaDeltaY是否大于0来判断是向上滑还是向下滑,手指滑动的时候它绝对不等于0
进入trackMotionScroll方法:
此时childCount肯定大于0,因为是滑动的时候才能进入此方法,firstTop是第一个item的顶部值,lastBottom是最后一个item的底部值。
这个down如果为true,表示向下滑动
这个if(down)是向下滑的,看else里向上滑的:
第4928行,child的bottom是小于top的,即该child是滑出了屏幕顶的,放入回收池,同理第4941行child的top是大于bottom的,即该child是滑出了屏幕的底边的,放入回收池。
detachViewsFromParent是把滑出屏幕的item解除与父节点的关系。
再进入fillCap方法:
就又进入前面有分析过的fillDown方法,在该方法里会调用makeAndView方法,在makeAndView方法里会从回收池里获取对应position的itemView然后显示到屏幕。
至此,本文所要解析的有关ListView的“观察者和模板模式、回收池、第一屏显示、刷新和非第一屏显示”都已详述完毕。