在android所有常用的原生控件当中,用法最复杂的应该就是listview了,它专门用于处理那种内容元素很多,手机屏幕无法展示出所有内容的情况。listview可以使用列表的形式来展示内容,超出屏幕部分的内容只需要通过手指滑动就可以移动到屏幕内了。
另外listview还有一个非常神奇的功能,我相信大家应该都体验过,即使在listview中加载非常非常多的数据,比如达到成百上千条甚至更多,listview都不会发生oom或者崩溃,而且随着我们手指滑动来浏览更多数据时,程序所占用的内存竟然都不会跟着增长。那么listview是怎么实现这么神奇的功能的呢?当初我就抱着学习的心态花了很长时间把listview的源码通读了一遍,基本了解了它的工作原理,在感叹google大神能够写出如此精妙代码的同时我也有所敬畏,因为listview的代码量比较大,复杂度也很高,很难用文字表达清楚,于是我就放弃了把它写成一篇博客的想法。那么现在回想起来这件事我已经肠子都悔青了,因为没过几个月时间我就把当初梳理清晰的源码又忘的一干二净。于是现在我又重新定下心来再次把listview的源码重读了一遍,那么这次我一定要把它写成一篇博客,分享给大家的同时也当成我自己的笔记吧。
首先我们先来看一下listview的继承结构,如下图所示:
可以看到,listview的继承结构还是相当复杂的,它是直接继承自的abslistview,而abslistview有两个子实现类,一个是listview,另一个就是gridview,因此我们从这一点就可以猜出来,listview和gridview在工作原理和实现上都是有很多共同点的。然后abslistview又继承自adapterview,adapterview继承自viewgroup,后面就是我们所熟知的了。先把listview的继承结构了解一下,待会儿有助于我们更加清晰地分析代码。
adapter的作用
adapter相信大家都不会陌生,我们平时使用listview的时候一定都会用到它。那么话说回来大家有没有仔细想过,为什么需要adapter这个东西呢?总感觉正因为有了adapter,listview的使用变得要比其它控件复杂得多。那么这里我们就先来学习一下adapter到底起到了什么样的一个作用。
其实说到底,控件就是为了交互和展示数据用的,只不过listview更加特殊,它是为了展示很多很多数据用的,但是listview只承担交互和展示工作而已,至于这些数据来自哪里,listview是不关心的。因此,我们能设想到的最基本的listview工作模式就是要有一个listview控件和一个数据源。
不过如果真的让listview和数据源直接打交道的话,那listview所要做的适配工作就非常繁杂了。因为数据源这个概念太模糊了,我们只知道它包含了很多数据而已,至于这个数据源到底是什么样类型,并没有严格的定义,有可能是数组,也有可能是集合,甚至有可能是数据库表中查询出来的游标。所以说如果listview真的去为每一种数据源都进行适配操作的话,一是扩展性会比较差,内置了几种适配就只有几种适配,不能动态进行添加。二是超出了它本身应该负责的工作范围,不再是仅仅承担交互和展示工作就可以了,这样listview就会变得比较臃肿。
那么显然android开发团队是不会允许这种事情发生的,于是就有了adapter这样一个机制的出现。顾名思义,adapter是适配器的意思,它在listview和数据源之间起到了一个桥梁的作用,listview并不会直接和数据源打交道,而是会借助adapter这个桥梁来去访问真正的数据源,与之前不同的是,adapter的接口都是统一的,因此listview不用再去担心任何适配方面的问题。而adapter又是一个接口(interface),它可以去实现各种各样的子类,每个子类都能通过自己的逻辑来去完成特定的功能,以及与特定数据源的适配操作,比如说arrayadapter可以用于数组和list类型的数据源适配,simplecursoradapter可以用于游标类型的数据源适配,这样就非常巧妙地把数据源适配困难的问题解决掉了,并且还拥有相当不错的扩展性。简单的原理示意图如下所示:
当然adapter的作用不仅仅只有数据源适配这一点,还有一个非常非常重要的方法也需要我们在adapter当中去重写,就是getview()方法,这个在下面的文章中还会详细讲到。
recyclebin机制
那么在开始分析listview的源码之前,还有一个东西是我们提前需要了解的,就是recyclebin机制,这个机制也是listview能够实现成百上千条数据都不会oom最重要的一个原因。其实recyclebin的代码并不多,只有300行左右,它是写在abslistview中的一个内部类,所以所有继承自abslistview的子类,也就是listview和gridview,都可以使用这个机制。那我们来看一下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.
*
* @see android.widget.abslistview#setrecyclerlistener(android.widget.abslistview.recyclerlistener)
* @see android.widget.abslistview.recyclerlistener
*/
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;
private arraylist<view> mcurrentscrap;
/**
* fill activeviews with all of the children of the abslistview.
*
* @param childcount
* the minimum number of views mactiveviews should hold
* @param firstactiveposition
* the position of the first view that will be stored in
* mactiveviews
*/
void fillactiveviews(int childcount, int firstactiveposition) {
if (mactiveviews.length < childcount) {
mactiveviews = new view[childcount];
}
mfirstactiveposition = firstactiveposition;
final view[] activeviews = mactiveviews;
for (int i = 0; i < childcount; i++) {
view child = getchildat(i);
abslistview.layoutparams lp = (abslistview.layoutparams) child.getlayoutparams();
// don't put header or footer views into the scrap heap
if (lp != null && lp.viewtype != item_view_type_header_or_footer) {
// note: we do place adapterview.item_view_type_ignore in
// active views.
// however, we will not place them into scrap views.
activeviews[i] = child;
}
}
}
/**
* get the view corresponding to the specified position. the view will
* be removed from mactiveviews if it is found.
*
* @param position
* the position to look up in mactiveviews
* @return the view if it is found, null otherwise
*/
view getactiveview(int position) {
int index = position - mfirstactiveposition;
final view[] activeviews = mactiveviews;
if (index >= 0 && index < activeviews.length) {
final view match = activeviews[index];
activeviews[index] = null;
return match;
}
return null;
}
/**
* put a view into the scapviews list. these views are unordered.
*
* @param scrap
* the view to add
*/
void addscrapview(view scrap) {
abslistview.layoutparams lp = (abslistview.layoutparams) scrap.getlayoutparams();
if (lp == null) {
return;
}
// don't put header or footer views or views that should be ignored
// into the scrap heap
int viewtype = lp.viewtype;
if (!shouldrecycleviewtype(viewtype)) {
if (viewtype != item_view_type_header_or_footer) {
removedetachedview(scrap, false);
}
return;
}
if (mviewtypecount == 1) {
dispatchfinishtemporarydetach(scrap);
mcurrentscrap.add(scrap);
} else {
dispatchfinishtemporarydetach(scrap);
mscrapviews[viewtype].add(scrap);
}
if (mrecyclerlistener != null) {
mrecyclerlistener.onmovedtoscrapheap(scrap);
}
}
/**
* @return a view from the scrapviews collection. these are unordered.
*/
view getscrapview(int position) {
arraylist<view> scrapviews;
if (mviewtypecount == 1) {
scrapviews = mcurrentscrap;
int size = scrapviews.size();
if (size > 0) {
return scrapviews.remove(size - 1);
} else {
return null;
}
} else {
int whichscrap = madapter.getitemviewtype(position);
if (whichscrap >= 0 && whichscrap < mscrapviews.length) {
scrapviews = mscrapviews[whichscrap];
int size = scrapviews.size();
if (size > 0) {
return scrapviews.remove(size - 1);
}
}
}
return null;
}
public void setviewtypecount(int viewtypecount) {
if (viewtypecount < 1) {
throw new illegalargumentexception("can't have a viewtypecount < 1");
}
// noinspection unchecked
arraylist<view>[] scrapviews = new arraylist[viewtypecount];
for (int i = 0; i < viewtypecount; i++) {
scrapviews[i] = new arraylist<view>();
}
mviewtypecount = viewtypecount;
mcurrentscrap = scrapviews[0];
mscrapviews = scrapviews;
}
}
这里的recyclebin代码并不全,我只是把最主要的几个方法提了出来。那么我们先来对这几个方法进行简单解读,这对后面分析listview的工作原理将会有很大的帮助。
fillactiveviews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示listview中第一个可见元素的position值。recyclebin当中使用mactiveviews这个数组来存储view,调用这个方法后就会根据传入的参数来将listview中的指定元素存储到mactiveviews数组当中。
getactiveview() 这个方法和fillactiveviews()是对应的,用于从mactiveviews数组当中获取数据。该方法接收一个position参数,表示元素在listview当中的位置,方法内部会自动将position值转换成mactiveviews数组对应的下标值。需要注意的是,mactiveviews当中所存储的view,一旦被获取了之后就会从mactiveviews当中移除,下次获取同样位置的view将会返回null,也就是说mactiveviews不能被重复利用。
addscrapview() 用于将一个废弃的view进行缓存,该方法接收一个view参数,当有某个view确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对view进行缓存,recyclebin当中使用mscrapviews和mcurrentscrap这两个list来存储废弃view。
getscrapview 用于从废弃缓存中取出一个view,这些废弃缓存中的view是没有顺序可言的,因此getscrapview()方法中的算法也非常简单,就是直接从mcurrentscrap当中获取尾部的一个scrap view进行返回。
setviewtypecount() 我们都知道adapter当中可以重写一个getviewtypecount()来表示listview中有几种类型的数据项,而setviewtypecount()方法的作用就是为每种类型的数据项都单独启用一个recyclebin缓存机制。实际上,getviewtypecount()方法通常情况下使用的并不是很多,所以我们只要知道recyclebin当中有这样一个功能就行了。
了解了recyclebin中的主要方法以及它们的用处之后,下面就可以开始来分析listview的工作原理了,这里我将还是按照以前分析源码的方式来进行,即跟着主线执行流程来逐步阅读并点到即止,不然的话要是把listview所有的代码都贴出来,那么本篇文章将会很长很长了。
第一次layout
view的执行流程无非就分为三步,onmeasure()用于测量view的大小,onlayout()用于确定view的布局,ondraw()用于将view绘制到界面上。而在listview当中,onmeasure()并没有什么特殊的地方,因为它终归是一个view,占用的空间最多并且通常也就是整个屏幕。ondraw()在listview当中也没有什么意义,因为listview本身并不负责绘制,而是由listview当中的子元素来进行绘制的。那么listview大部分的神奇功能其实都是在onlayout()方法中进行的了,因此我们本篇文章也是主要分析的这个方法里的内容。
如果你到listview源码中去找一找,你会发现listview中是没有onlayout()这个方法的,这是因为这个方法是在listview的父类abslistview中实现的,代码如下所示:
/**
* subclasses should not override this method but {@link #layoutchildren()}
* instead.
*/
@override
protected void onlayout(boolean changed, int l, int t, int r, int b) {
super.onlayout(changed, l, t, r, b);
minlayout = true;
if (changed) {
int childcount = getchildcount();
for (int i = 0; i < childcount; i++) {
getchildat(i).forcelayout();
}
mrecycler.markchildrendirty();
}
layoutchildren();
minlayout = false;
}
可以看到,onlayout()方法中并没有做什么复杂的逻辑操作,主要就是一个判断,如果listview的大小或者位置发生了变化,那么changed变量就会变成true,此时会要求所有的子布局都强制进行重绘。除此之外倒没有什么难理解的地方了,不过我们注意到,在第16行调用了layoutchildren()这个方法,从方法名上我们就可以猜出这个方法是用来进行子元素布局的,不过进入到这个方法当中你会发现这是个空方法,没有一行代码。这当然是可以理解的了,因为子元素的布局应该是由具体的实现类来负责完成的,而不是由父类完成。那么进入listview的layoutchildren()方法,代码如下所示:
@override
protected void layoutchildren() {
final boolean blocklayoutrequests = mblocklayoutrequests;
if (!blocklayoutrequests) {
mblocklayoutrequests = true;
} else {
return;
}
try {
super.layoutchildren();
invalidate();
if (madapter == null) {
resetlist();
invokeonitemscrolllistener();
return;
}
int childrentop = mlistpadding.top;
int childrenbottom = getbottom() - gettop() - mlistpadding.bottom;
int childcount = getchildcount();
int index = 0;
int delta = 0;
view sel;
view oldsel = null;
view oldfirst = null;
view newsel = null;
view focuslayoutrestoreview = null;
// remember stuff we will need down below
switch (mlayoutmode) {
case layout_set_selection:
index = mnextselectedposition - mfirstposition;
if (index >= 0 && index < childcount) {
newsel = getchildat(index);
}
break;
case layout_force_top:
case layout_force_bottom:
case layout_specific:
case layout_sync:
break;
case layout_move_selection:
default:
// remember the previously selected view
index = mselectedposition - mfirstposition;
if (index >= 0 && index < childcount) {
oldsel = getchildat(index);
}
// remember the previous first child
oldfirst = getchildat(0);
if (mnextselectedposition >= 0) {
delta = mnextselectedposition - mselectedposition;
}
// caution: newsel might be null
newsel = getchildat(index + delta);
}
boolean datachanged = mdatachanged;
if (datachanged) {
handledatachanged();
}
// handle the empty set by removing all views that are visible
// and calling it a day
if (mitemcount == 0) {
resetlist();
invokeonitemscrolllistener();
return;
} else if (mitemcount != madapter.getcount()) {
throw new illegalstateexception("the content of the adapter has changed but "
+ "listview did not receive a notification. make sure the content of "
+ "your adapter is not modified from a background thread, but only "
+ "from the ui thread. [in listview(" + getid() + ", " + getclass()
+ ") with adapter(" + madapter.getclass() + ")]");
}
setselectedpositionint(mnextselectedposition);
// pull all children into the recyclebin.
// these views will be reused if possible
final int firstposition = mfirstposition;
final recyclebin recyclebin = mrecycler;
// reset the focus restoration
view focuslayoutrestoredirectchild = null;
// don't put header or footer views into the recycler. those are
// already cached in mheaderviews;
if (datachanged) {
for (int i = 0; i < childcount; i++) {
recyclebin.addscrapview(getchildat(i));
if (viewdebug.trace_recycler) {
viewdebug.trace(getchildat(i),
viewdebug.recyclertracetype.move_to_scrap_heap, index, i);
}
}
} else {
recyclebin.fillactiveviews(childcount, firstposition);
}
// take focus back to us temporarily to avoid the eventual
// call to clear focus when removing the focused child below
// from messing things up when viewroot assigns focus back
// to someone else
final view focusedchild = getfocusedchild();
if (focusedchild != null) {
// todo: in some cases focusedchild.getparent() == null
// we can remember the focused view to restore after relayout if the
// data hasn't changed, or if the focused position is a header or footer
if (!datachanged || isdirectchildheaderorfooter(focusedchild)) {
focuslayoutrestoredirectchild = focusedchild;
// remember the specific view that had focus
focuslayoutrestoreview = findfocus();
if (focuslayoutrestoreview != null) {
// tell it we are going to mess with it
focuslayoutrestoreview.onstarttemporarydetach();
}
}
requestfocus();
}
// clear out old views
detachallviewsfromparent();
switch (mlayoutmode) {
case layout_set_selection:
if (newsel != null) {
sel = fillfromselection(newsel.gettop(), childrentop, childrenbottom);
} else {
sel = fillfrommiddle(childrentop, childrenbottom);
}
break;
case layout_sync:
sel = fillspecific(msyncposition, mspecifictop);
break;
case layout_force_bottom:
sel = fillup(mitemcount - 1, childrenbottom);
adjustviewsupordown();
break;
case layout_force_top:
mfirstposition = 0;
sel = fillfromtop(childrentop);
adjustviewsupordown();
break;
case layout_specific:
sel = fillspecific(reconcileselectedposition(), mspecifictop);
break;
case layout_move_selection:
sel = moveselection(oldsel, newsel, delta, childrentop, childrenbottom);
break;
default:
if (childcount == 0) {
if (!mstackfrombottom) {
final int position = lookforselectableposition(0, true);
setselectedpositionint(position);
sel = fillfromtop(childrentop);
} else {
final int position = lookforselectableposition(mitemcount - 1, false);
setselectedpositionint(position);
sel = fillup(mitemcount - 1, childrenbottom);
}
} else {
if (mselectedposition >= 0 && mselectedposition < mitemcount) {
sel = fillspecific(mselectedposition,
oldsel == null ? childrentop : oldsel.gettop());
} else if (mfirstposition < mitemcount) {
sel = fillspecific(mfirstposition,
oldfirst == null ? childrentop : oldfirst.gettop());
} else {
sel = fillspecific(0, childrentop);
}
}
break;
}
// flush any cached views that did not get reused above
recyclebin.scrapactiveviews();
if (sel != null) {
// the current selected item should get focus if items
// are focusable
if (mitemscanfocus && hasfocus() && !sel.hasfocus()) {
final boolean focuswastaken = (sel == focuslayoutrestoredirectchild &&
focuslayoutrestoreview.requestfocus()) || sel.requestfocus();
if (!focuswastaken) {
// selected item didn't take focus, fine, but still want
// to make sure something else outside of the selected view
// has focus
final view focused = getfocusedchild();
if (focused != null) {
focused.clearfocus();
}
positionselector(sel);
} else {
sel.setselected(false);
mselectorrect.setempty();
}
} else {
positionselector(sel);
}
mselectedtop = sel.gettop();
} else {
if (mtouchmode > touch_mode_down && mtouchmode < touch_mode_scroll) {
view child = getchildat(mmotionposition - mfirstposition);
if (child != null) positionselector(child);
} else {
mselectedtop = 0;
mselectorrect.setempty();
}
// even if there is not selected position, we may need to restore
// focus (i.e. something focusable in touch mode)
if (hasfocus() && focuslayoutrestoreview != null) {
focuslayoutrestoreview.requestfocus();
}
}
// tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focuslayoutrestoreview != null
&& focuslayoutrestoreview.getwindowtoken() != null) {
focuslayoutrestoreview.onfinishtemporarydetach();
}
mlayoutmode = layout_normal;
mdatachanged = false;
mneedsync = false;
setnextselectedpositionint(mselectedposition);
updatescrollindicators();
if (mitemcount > 0) {
checkselectionchanged();
}
invokeonitemscrolllistener();
} finally {
if (!blocklayoutrequests) {
mblocklayoutrequests = false;
}
}
}
这段代码比较长,我们挑重点的看。首先可以确定的是,listview当中目前还没有任何子view,数据都还是由adapter管理的,并没有展示到界面上,因此第19行getchildcount()方法得到的值肯定是0。接着在第81行会根据datachanged这个布尔型的值来判断执行逻辑,datachanged只有在数据源发生改变的情况下才会变成true,其它情况都是false,因此这里会进入到第90行的执行逻辑,调用recyclebin的fillactiveviews()方法。按理来说,调用fillactiveviews()方法是为了将listview的子view进行缓存的,可是目前listview中还没有任何的子view,因此这一行暂时还起不了任何作用。
接下来在第114行会根据mlayoutmode的值来决定布局模式,默认情况下都是普通模式layout_normal,因此会进入到第140行的default语句当中。而下面又会紧接着进行两次if判断,childcount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到第145行的fillfromtop()方法,我们跟进去瞧一瞧:
/**
* fills the list from top to bottom, starting with mfirstposition
*
* @param nexttop the location where the top of the first item should be
* drawn
*
* @return the view that is currently selected
*/
private view fillfromtop(int nexttop) {
mfirstposition = math.min(mfirstposition, mselectedposition);
mfirstposition = math.min(mfirstposition, mitemcount - 1);
if (mfirstposition < 0) {
mfirstposition = 0;
}
return filldown(mfirstposition, nexttop);
}
从这个方法的注释中可以看出,它所负责的主要任务就是从mfirstposition开始,自顶至底去填充listview。而这个方法本身并没有什么逻辑,就是判断了一下mfirstposition值的合法性,然后调用filldown()方法,那么我们就有理由可以猜测,填充listview的操作是在filldown()方法中完成的。进入filldown()方法,代码如下所示:
/**
* fills the list from pos down to the end of the list view.
*
* @param pos the first position to put in the list
*
* @param nexttop the location where the top of the item associated with pos
* should be drawn
*
* @return the view that is currently selected, if it happens to be in the
* range that we draw.
*/
private view filldown(int pos, int nexttop) {
view selectedview = null;
int end = (getbottom() - gettop()) - mlistpadding.bottom;
while (nexttop < end && pos < mitemcount) {
// is this the selected item?
boolean selected = pos == mselectedposition;
view child = makeandaddview(pos, nexttop, true, mlistpadding.left, selected);
nexttop = child.getbottom() + mdividerheight;
if (selected) {
selectedview = child;
}
pos++;
}
return selectedview;
}
可以看到,这里使用了一个while循环来执行重复逻辑,一开始nexttop的值是第一个子元素顶部距离整个listview顶部的像素值,pos则是刚刚传入的mfirstposition的值,而end是listview底部减去顶部所得的像素值,mitemcount则是adapter中的元素数量。因此一开始的情况下nexttop必定是小于end值的,并且pos也是小于mitemcount值的。那么每执行一次while循环,pos的值都会加1,并且nexttop也会增加,当nexttop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mitemcount时,也就是所有adapter中的元素都被遍历结束了,就会跳出while循环。
那么while循环当中又做了什么事情呢?值得让人留意的就是第18行调用的makeandaddview()方法,进入到这个方法当中,代码如下所示:
/**
* obtain the view and add it to our list of children. the view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position logical position in the list
* @param y top or bottom edge of the view to add
* @param flow if flow is true, align top edge to y. if false, align bottom
* edge to y.
* @param childrenleft left edge where children should be positioned
* @param selected is this position selected?
* @return view that was added
*/
private view makeandaddview(int position, int y, boolean flow, int childrenleft,
boolean selected) {
view child;
if (!mdatachanged) {
// try to use an exsiting view for this position
child = mrecycler.getactiveview(position);
if (child != null) {
// 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;
}
这里在第19行尝试从recyclebin当中快速获取一个active view,不过很遗憾的是目前recyclebin当中还没有缓存任何的view,所以这里得到的值肯定是null。那么取得了null之后就会继续向下运行,到第28行会调用obtainview()方法来再次尝试获取一个view,这次的obtainview()方法是可以保证一定返回一个view的,于是下面立刻将获取到的view传入到了setupchild()方法当中。那么obtainview()内部到底是怎么工作的呢?我们先进入到这个方法里面看一下:
/**
* get a view and have it show the data associated with the specified
* position. this is called when we have already discovered that the view is
* not available for reuse in the recycle bin. the only choices left are
* converting an old view or making a new one.
*
* @param position
* the position to display
* @param isscrap
* array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return a view displaying the data associated with the specified position
*/
view obtainview(int position, boolean[] isscrap) {
isscrap[0] = false;
view scrapview;
scrapview = mrecycler.getscrapview(position);
view child;
if (scrapview != null) {
child = madapter.getview(position, scrapview, this);
if (child != scrapview) {
mrecycler.addscrapview(scrapview);
if (mcachecolorhint != 0) {
child.setdrawingcachebackgroundcolor(mcachecolorhint);
}
} else {
isscrap[0] = true;
dispatchfinishtemporarydetach(child);
}
} else {
child = madapter.getview(position, null, this);
if (mcachecolorhint != 0) {
child.setdrawingcachebackgroundcolor(mcachecolorhint);
}
}
return child;
}
obtainview()方法中的代码并不多,但却包含了非常非常重要的逻辑,不夸张的说,整个listview中最重要的内容可能就在这个方法里了。那么我们还是按照执行流程来看,在第19行代码中调用了recyclebin的getscrapview()方法来尝试获取一个废弃缓存中的view,同样的道理,这里肯定是获取不到的,getscrapview()方法会返回一个null。这时该怎么办呢?没有关系,代码会执行到第33行,调用madapter的getview()方法来去获取一个view。那么madapter是什么呢?当然就是当前listview关联的适配器了。而getview()方法又是什么呢?还用说吗,这个就是我们平时使用listview时最最经常重写的一个方法了,这里getview()方法中传入了三个参数,分别是position,null和this。
那么我们平时写listview的adapter时,getview()方法通常会怎么写呢?这里我举个简单的例子:
@override
public view getview(int position, view convertview, viewgroup parent) {
fruit fruit = getitem(position);
view view;
if (convertview == null) {
view = layoutinflater.from(getcontext()).inflate(resourceid, null);
} else {
view = convertview;
}
imageview fruitimage = (imageview) view.findviewbyid(r.id.fruit_image);
textview fruitname = (textview) view.findviewbyid(r.id.fruit_name);
fruitimage.setimageresource(fruit.getimageid());
fruitname.settext(fruit.getname());
return view;
}
getview()方法接受的三个参数,第一个参数position代表当前子元素的的位置,我们可以通过具体的位置来获取与其相关的数据。第二个参数convertview,刚才传入的是null,说明没有convertview可以利用,因此我们会调用layoutinflater的inflate()方法来去加载一个布局。接下来会对这个view进行一些属性和值的设定,最后将view返回。
那么这个view也会作为obtainview()的结果进行返回,并最终传入到setupchild()方法当中。其实也就是说,第一次layout过程当中,所有的子view都是调用layoutinflater的inflate()方法加载出来的,这样就会相对比较耗时,但是不用担心,后面就不会再有这种情况了,那么我们继续往下看:
/**
* add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child the view to add
* @param position the position of this child
* @param y the y position relative to which this view will be positioned
* @param flowdown if true, align top edge to y. if false, align bottom
* edge to y.
* @param childrenleft left edge where children should be positioned
* @param selected is this position selected?
* @param recycled has this view been pulled from the recycle bin? if so it
* does not need to be remeasured.
*/
private void setupchild(view child, int position, int y, boolean flowdown, int childrenleft,
boolean selected, boolean recycled) {
final boolean isselected = selected && shouldshowselector();
final boolean updatechildselected = isselected != child.isselected();
final int mode = mtouchmode;
final boolean ispressed = mode > touch_mode_down && mode < touch_mode_scroll &&
mmotionposition == position;
final boolean updatechildpressed = ispressed != child.ispressed();
final boolean needtomeasure = !recycled || updatechildselected || child.islayoutrequested();
// respect layout params that are already in the view. otherwise make some up...
// noinspection unchecked
abslistview.layoutparams p = (abslistview.layoutparams) child.getlayoutparams();
if (p == null) {
p = new abslistview.layoutparams(viewgroup.layoutparams.match_parent,
viewgroup.layoutparams.wrap_content, 0);
}
p.viewtype = madapter.getitemviewtype(position);
if ((recycled && !p.forceadd) || (p.recycledheaderfooter &&
p.viewtype == adapterview.item_view_type_header_or_footer)) {
attachviewtoparent(child, flowdown ? -1 : 0, p);
} else {
p.forceadd = false;
if (p.viewtype == adapterview.item_view_type_header_or_footer) {
p.recycledheaderfooter = true;
}
addviewinlayout(child, flowdown ? -1 : 0, p, true);
}
if (updatechildselected) {
child.setselected(isselected);
}
if (updatechildpressed) {
child.setpressed(ispressed);
}
if (needtomeasure) {
int childwidthspec = viewgroup.getchildmeasurespec(mwidthmeasurespec,
mlistpadding.left + mlistpadding.right, p.width);
int lpheight = p.height;
int childheightspec;
if (lpheight > 0) {
childheightspec = measurespec.makemeasurespec(lpheight, measurespec.exactly);
} else {
childheightspec = measurespec.makemeasurespec(0, measurespec.unspecified);
}
child.measure(childwidthspec, childheightspec);
} else {
cleanuplayoutstate(child);
}
final int w = child.getmeasuredwidth();
final int h = child.getmeasuredheight();
final int childtop = flowdown ? y : y - h;
if (needtomeasure) {
final int childright = childrenleft + w;
final int childbottom = childtop + h;
child.layout(childrenleft, childtop, childright, childbottom);
} else {
child.offsetleftandright(childrenleft - child.getleft());
child.offsettopandbottom(childtop - child.gettop());
}
if (mcachingstarted && !child.isdrawingcacheenabled()) {
child.setdrawingcacheenabled(true);
}
}
setupchild()方法当中的代码虽然比较多,但是我们只看核心代码的话就非常简单了,刚才调用obtainview()方法获取到的子元素view,这里在第40行调用了addviewinlayout()方法将它添加到了listview当中。那么根据filldown()方法中的while循环,会让子元素view将整个listview控件填满然后就跳出,也就是说即使我们的adapter中有一千条数据,listview也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证listview中的内容能够迅速展示到屏幕上。
那么到此为止,第一次layout过程结束。
第二次layout
虽然我在源码中并没有找出具体的原因,但如果你自己做一下实验的话就会发现,即使是一个再简单的view,在展示到界面上之前都会经历至少两次onmeasure()和两次onlayout()的过程。其实这只是一个很小的细节,平时对我们影响并不大,因为不管是onmeasure()或者onlayout()几次,反正都是执行的相同的逻辑,我们并不需要进行过多关心。但是在listview中情况就不一样了,因为这就意味着layoutchildren()过程会执行两次,而这个过程当中涉及到向listview中添加子元素,如果相同的逻辑执行两遍的话,那么listview中就会存在一份重复的数据了。因此listview在layoutchildren()过程当中做了第二次layout的逻辑处理,非常巧妙地解决了这个问题,下面我们就来分析一下第二次layout的过程。
其实第二次layout和第一次layout的基本流程是差不多的,那么我们还是从layoutchildren()方法开始看起:
@override
protected void layoutchildren() {
final boolean blocklayoutrequests = mblocklayoutrequests;
if (!blocklayoutrequests) {
mblocklayoutrequests = true;
} else {
return;
}
try {
super.layoutchildren();
invalidate();
if (madapter == null) {
resetlist();
invokeonitemscrolllistener();
return;
}
int childrentop = mlistpadding.top;
int childrenbottom = getbottom() - gettop() - mlistpadding.bottom;
int childcount = getchildcount();
int index = 0;
int delta = 0;
view sel;
view oldsel = null;
view oldfirst = null;
view newsel = null;
view focuslayoutrestoreview = null;
// remember stuff we will need down below
switch (mlayoutmode) {
case layout_set_selection:
index = mnextselectedposition - mfirstposition;
if (index >= 0 && index < childcount) {
newsel = getchildat(index);
}
break;
case layout_force_top:
case layout_force_bottom:
case layout_specific:
case layout_sync:
break;
case layout_move_selection:
default:
// remember the previously selected view
index = mselectedposition - mfirstposition;
if (index >= 0 && index < childcount) {
oldsel = getchildat(index);
}
// remember the previous first child
oldfirst = getchildat(0);
if (mnextselectedposition >= 0) {
delta = mnextselectedposition - mselectedposition;
}
// caution: newsel might be null
newsel = getchildat(index + delta);
}
boolean datachanged = mdatachanged;
if (datachanged) {
handledatachanged();
}
// handle the empty set by removing all views that are visible
// and calling it a day
if (mitemcount == 0) {
resetlist();
invokeonitemscrolllistener();
return;
} else if (mitemcount != madapter.getcount()) {
throw new illegalstateexception("the content of the adapter has changed but "
+ "listview did not receive a notification. make sure the content of "
+ "your adapter is not modified from a background thread, but only "
+ "from the ui thread. [in listview(" + getid() + ", " + getclass()
+ ") with adapter(" + madapter.getclass() + ")]");
}
setselectedpositionint(mnextselectedposition);
// pull all children into the recyclebin.
// these views will be reused if possible
final int firstposition = mfirstposition;
final recyclebin recyclebin = mrecycler;
// reset the focus restoration
view focuslayoutrestoredirectchild = null;
// don't put header or footer views into the recycler. those are
// already cached in mheaderviews;
if (datachanged) {
for (int i = 0; i < childcount; i++) {
recyclebin.addscrapview(getchildat(i));
if (viewdebug.trace_recycler) {
viewdebug.trace(getchildat(i),
viewdebug.recyclertracetype.move_to_scrap_heap, index, i);
}
}
} else {
recyclebin.fillactiveviews(childcount, firstposition);
}
// take focus back to us temporarily to avoid the eventual
// call to clear focus when removing the focused child below
// from messing things up when viewroot assigns focus back
// to someone else
final view focusedchild = getfocusedchild();
if (focusedchild != null) {
// todo: in some cases focusedchild.getparent() == null
// we can remember the focused view to restore after relayout if the
// data hasn't changed, or if the focused position is a header or footer
if (!datachanged || isdirectchildheaderorfooter(focusedchild)) {
focuslayoutrestoredirectchild = focusedchild;
// remember the specific view that had focus
focuslayoutrestoreview = findfocus();
if (focuslayoutrestoreview != null) {
// tell it we are going to mess with it
focuslayoutrestoreview.onstarttemporarydetach();
}
}
requestfocus();
}
// clear out old views
detachallviewsfromparent();
switch (mlayoutmode) {
case layout_set_selection:
if (newsel != null) {
sel = fillfromselection(newsel.gettop(), childrentop, childrenbottom);
} else {
sel = fillfrommiddle(childrentop, childrenbottom);
}
break;
case layout_sync:
sel = fillspecific(msyncposition, mspecifictop);
break;
case layout_force_bottom:
sel = fillup(mitemcount - 1, childrenbottom);
adjustviewsupordown();
break;
case layout_force_top:
mfirstposition = 0;
sel = fillfromtop(childrentop);
adjustviewsupordown();
break;
case layout_specific:
sel = fillspecific(reconcileselectedposition(), mspecifictop);
break;
case layout_move_selection:
sel = moveselection(oldsel, newsel, delta, childrentop, childrenbottom);
break;
default:
if (childcount == 0) {
if (!mstackfrombottom) {
final int position = lookforselectableposition(0, true);
setselectedpositionint(position);
sel = fillfromtop(childrentop);
} else {
final int position = lookforselectableposition(mitemcount - 1, false);
setselectedpositionint(position);
sel = fillup(mitemcount - 1, childrenbottom);
}
} else {
if (mselectedposition >= 0 && mselectedposition < mitemcount) {
sel = fillspecific(mselectedposition,
oldsel == null ? childrentop : oldsel.gettop());
} else if (mfirstposition < mitemcount) {
sel = fillspecific(mfirstposition,
oldfirst == null ? childrentop : oldfirst.gettop());
} else {
sel = fillspecific(0, childrentop);
}
}
break;
}
// flush any cached views that did not get reused above
recyclebin.scrapactiveviews();
if (sel != null) {
// the current selected item should get focus if items
// are focusable
if (mitemscanfocus && hasfocus() && !sel.hasfocus()) {
final boolean focuswastaken = (sel == focuslayoutrestoredirectchild &&
focuslayoutrestoreview.requestfocus()) || sel.requestfocus();
if (!focuswastaken) {
// selected item didn't take focus, fine, but still want
// to make sure something else outside of the selected view
// has focus
final view focused = getfocusedchild();
if (focused != null) {
focused.clearfocus();
}
positionselector(sel);
} else {
sel.setselected(false);
mselectorrect.setempty();
}
} else {
positionselector(sel);
}
mselectedtop = sel.gettop();
} else {
if (mtouchmode > touch_mode_down && mtouchmode < touch_mode_scroll) {
view child = getchildat(mmotionposition - mfirstposition);
if (child != null) positionselector(child);
} else {
mselectedtop = 0;
mselectorrect.setempty();
}
// even if there is not selected position, we may need to restore
// focus (i.e. something focusable in touch mode)
if (hasfocus() && focuslayoutrestoreview != null) {
focuslayoutrestoreview.requestfocus();
}
}
// tell focus view we are done mucking with it, if it is still in
// our view hierarchy.
if (focuslayoutrestoreview != null
&& focuslayoutrestoreview.getwindowtoken() != null) {
focuslayoutrestoreview.onfinishtemporarydetach();
}
mlayoutmode = layout_normal;
mdatachanged = false;
mneedsync = false;
setnextselectedpositionint(mselectedposition);
updatescrollindicators();
if (mitemcount > 0) {
checkselectionchanged();
}
invokeonitemscrolllistener();
} finally {
if (!blocklayoutrequests) {
mblocklayoutrequests = false;
}
}
}
同样还是在第19行,调用getchildcount()方法来获取子view的数量,只不过现在得到的值不会再是0了,而是listview中一屏可以显示的子view数量,因为我们刚刚在第一次layout过程当中向listview添加了这么多的子view。下面在第90行调用了recyclebin的fillactiveviews()方法,这次效果可就不一样了,因为目前listview中已经有子view了,这样所有的子view都会被缓存到recyclebin的mactiveviews数组当中,后面将会用到它们。
接下来将会是非常非常重要的一个操作,在第113行调用了detachallviewsfromparent()方法。这个方法会将所有listview当中的子view全部清除掉,从而保证第二次layout过程不会产生一份重复的数据。那有的朋友可能会问了,这样把已经加载好的view又清除掉,待会还要再重新加载一遍,这不是严重影响效率吗?不用担心,还记得我们刚刚调用了recyclebin的fillactiveviews()方法来缓存子view吗,待会儿将会直接使用这些缓存好的view来进行加载,而并不会重新执行一遍inflate过程,因此效率方面并不会有什么明显的影响。
那么我们接着看,在第141行的判断逻辑当中,由于不再等于0了,因此会进入到else语句当中。而else语句中又有三个逻辑判断,第一个逻辑判断不成立,因为默认情况下我们没有选中任何子元素,mselectedposition应该等于-1。第二个逻辑判断通常是成立的,因为mfirstposition的值一开始是等于0的,只要adapter中的数据大于0条件就成立。那么进入到fillspecific()方法当中,代码如下所示:
/**
* put a specific item at a specific location on the screen and then build
* up and down from there.
*
* @param position the reference view to use as the starting point
* @param top pixel offset from the top of this view to the top of the
* reference view.
*
* @return the selected view, or null if the selected view is outside the
* visible area.
*/
private view fillspecific(int position, int top) {
boolean tempisselected = position == mselectedposition;
view temp = makeandaddview(position, top, true, mlistpadding.left, tempisselected);
// possibly changed again in fillup if we add rows above this one.
mfirstposition = position;
view above;
view below;
final int dividerheight = mdividerheight;
if (!mstackfrombottom) {
above = fillup(position - 1, temp.gettop() - dividerheight);
// this will correct for the top of the first view not touching the top of the list
adjustviewsupordown();
below = filldown(position + 1, temp.getbottom() + dividerheight);
int childcount = getchildcount();
if (childcount > 0) {
correcttoohigh(childcount);
}
} else {
below = filldown(position + 1, temp.getbottom() + dividerheight);
// this will correct for the bottom of the last view not touching the bottom of the list
adjustviewsupordown();
above = fillup(position - 1, temp.gettop() - dividerheight);
int childcount = getchildcount();
if (childcount > 0) {
correcttoolow(childcount);
}
}
if (tempisselected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
fillspecific()这算是一个新方法了,不过其实它和fillup()、filldown()方法功能也是差不多的,主要的区别在于,fillspecific()方法会优先将指定位置的子view先加载到屏幕上,然后再加载该子view往上以及往下的其它子view。那么由于这里我们传入的position就是第一个子view的位置,于是fillspecific()方法的作用就基本上和filldown()方法是差不多的了,这里我们就不去关注太多它的细节,而是将精力放在makeandaddview()方法上面。再次回到makeandaddview()方法,代码如下所示:
/**
* obtain the view and add it to our list of children. the view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position logical position in the list
* @param y top or bottom edge of the view to add
* @param flow if flow is true, align top edge to y. if false, align bottom
* edge to y.
* @param childrenleft left edge where children should be positioned
* @param selected is this position selected?
* @return view that was added
*/
private view makeandaddview(int position, int y, boolean flow, int childrenleft,
boolean selected) {
view child;
if (!mdatachanged) {
// try to use an exsiting view for this position
child = mrecycler.getactiveview(position);
if (child != null) {
// 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;
}
仍然还是在第19行尝试从recyclebin当中获取active view,然而这次就一定可以获取到了,因为前面我们调用了recyclebin的fillactiveviews()方法来缓存子view。那么既然如此,就不会再进入到第28行的obtainview()方法,而是会直接进入setupchild()方法当中,这样也省去了很多时间,因为如果在obtainview()方法中又要去infalte布局的话,那么listview的初始加载效率就大大降低了。
注意在第23行,setupchild()方法的最后一个参数传入的是true,这个参数表明当前的view是之前被回收过的,那么我们再次回到setupchild()方法当中:
/**
* add a view as a child and make sure it is measured (if necessary) and
* positioned properly.
*
* @param child the view to add
* @param position the position of this child
* @param y the y position relative to which this view will be positioned
* @param flowdown if true, align top edge to y. if false, align bottom
* edge to y.
* @param childrenleft left edge where children should be positioned
* @param selected is this position selected?
* @param recycled has this view been pulled from the recycle bin? if so it
* does not need to be remeasured.
*/
private void setupchild(view child, int position, int y, boolean flowdown, int childrenleft,
boolean selected, boolean recycled) {
final boolean isselected = selected && shouldshowselector();
final boolean updatechildselected = isselected != child.isselected();
final int mode = mtouchmode;
final boolean ispressed = mode > touch_mode_down && mode < touch_mode_scroll &&
mmotionposition == position;
final boolean updatechildpressed = ispressed != child.ispressed();
final boolean needtomeasure = !recycled || updatechildselected || child.islayoutrequested();
// respect layout params that are already in the view. otherwise make some up...
// noinspection unchecked
abslistview.layoutparams p = (abslistview.layoutparams) child.getlayoutparams();
if (p == null) {
p = new abslistview.layoutparams(viewgroup.layoutparams.match_parent,
viewgroup.layoutparams.wrap_content, 0);
}
p.viewtype = madapter.getitemviewtype(position);
if ((recycled && !p.forceadd) || (p.recycledheaderfooter &&
p.viewtype == adapterview.item_view_type_header_or_footer)) {
attachviewtoparent(child, flowdown ? -1 : 0, p);
} else {
p.forceadd = false;
if (p.viewtype == adapterview.item_view_type_header_or_footer) {
p.recycledheaderfooter = true;
}
addviewinlayout(child, flowdown ? -1 : 0, p, true);
}
if (updatechildselected) {
child.setselected(isselected);
}
if (updatechildpressed) {
child.setpressed(ispressed);
}
if (needtomeasure) {
int childwidthspec = viewgroup.getchildmeasurespec(mwidthmeasurespec,
mlistpadding.left + mlistpadding.right, p.width);
int lpheight = p.height;
int childheightspec;
if (lpheight > 0) {
childheightspec = measurespec.makemeasurespec(lpheight, measurespec.exactly);
} else {
childheightspec = measurespec.makemeasurespec(0, measurespec.unspecified);
}
child.measure(childwidthspec, childheightspec);
} else {
cleanuplayoutstate(child);
}
final int w = child.getmeasuredwidth();
final int h = child.getmeasuredheight();
final int childtop = flowdown ? y : y - h;
if (needtomeasure) {
final int childright = childrenleft + w;
final int childbottom = childtop + h;
child.layout(childrenleft, childtop, childright, childbottom);
} else {
child.offsetleftandright(childrenleft - child.getleft());
child.offsettopandbottom(childtop - child.gettop());
}
if (mcachingstarted && !child.isdrawingcacheenabled()) {
child.setdrawingcacheenabled(true);
}
}
可以看到,setupchild()方法的最后一个参数是recycled,然后在第32行会对这个变量进行判断,由于recycled现在是true,所以会执行attachviewtoparent()方法,而第一次layout过程则是执行的else语句中的addviewinlayout()方法。这两个方法最大的区别在于,如果我们需要向viewgroup中添加一个新的子view,应该调用addviewinlayout()方法,而如果是想要将一个之前detach的view重新attach到viewgroup上,就应该调用attachviewtoparent()方法。那么由于前面在layoutchildren()方法当中调用了detachallviewsfromparent()方法,这样listview中所有的子view都是处于detach状态的,所以这里attachviewtoparent()方法是正确的选择。
经历了这样一个detach又attach的过程,listview中所有的子view又都可以正常显示出来了,那么第二次layout过程结束。
滑动加载更多数据
经历了两次layout过程,虽说我们已经可以在listview中看到内容了,然而关于listview最神奇的部分我们却还没有接触到,因为目前listview中只是加载并显示了第一屏的数据而已。比如说我们的adapter当中有1000条数据,但是第一屏只显示了10条,listview中也只有10个子view而已,那么剩下的990是怎样工作并显示到界面上的呢?这就要看一下listview滑动部分的源码了,因为我们是通过手指滑动来显示更多数据的。
由于滑动部分的机制是属于通用型的,即listview和gridview都会使用同样的机制,因此这部分代码就肯定是写在abslistview当中的了。那么监听触控事件是在ontouchevent()方法当中进行的,我们就来看一下abslistview中的这个方法:
@override
public boolean ontouchevent(motionevent ev) {
if (!isenabled()) {
// a disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return isclickable() || islongclickable();
}
final int action = ev.getaction();
view v;
int deltay;
if (mvelocitytracker == null) {
mvelocitytracker = velocitytracker.obtain();
}
mvelocitytracker.addmovement(ev);
switch (action & motionevent.action_mask) {
case motionevent.action_down: {
mactivepointerid = ev.getpointerid(0);
final int x = (int) ev.getx();
final int y = (int) ev.gety();
int motionposition = pointtoposition(x, y);
if (!mdatachanged) {
if ((mtouchmode != touch_mode_fling) && (motionposition >= 0)
&& (getadapter().isenabled(motionposition))) {
// user clicked on an actual view (and was not stopping a
// fling). it might be a
// click or a scroll. assume it is a click until proven
// otherwise
mtouchmode = touch_mode_down;
// fixme debounce
if (mpendingcheckfortap == null) {
mpendingcheckfortap = new checkfortap();
}
postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout());
} else {
if (ev.getedgeflags() != 0 && motionposition < 0) {
// if we couldn't find a view to click on, but the down
// event was touching
// the edge, we will bail out and try again. this allows
// the edge correcting
// code in viewroot to try to find a nearby view to
// select
return false;
}
if (mtouchmode == touch_mode_fling) {
// stopped a fling. it is a scroll.
createscrollingcache();
mtouchmode = touch_mode_scroll;
mmotioncorrection = 0;
motionposition = findmotionrow(y);
reportscrollstatechange(onscrolllistener.scroll_state_touch_scroll);
}
}
}
if (motionposition >= 0) {
// remember where the motion event started
v = getchildat(motionposition - mfirstposition);
mmotionvieworiginaltop = v.gettop();
}
mmotionx = x;
mmotiony = y;
mmotionposition = motionposition;
mlasty = integer.min_value;
break;
}
case motionevent.action_move: {
final int pointerindex = ev.findpointerindex(mactivepointerid);
final int y = (int) ev.gety(pointerindex);
deltay = y - mmotiony;
switch (mtouchmode) {
case touch_mode_down:
case touch_mode_tap:
case touch_mode_done_waiting:
// check if we have moved far enough that it looks more like a
// scroll than a tap
startscrollifneeded(deltay);
break;
case touch_mode_scroll:
if (profile_scrolling) {
if (!mscrollprofilingstarted) {
debug.startmethodtracing("abslistviewscroll");
mscrollprofilingstarted = true;
}
}
if (y != mlasty) {
deltay -= mmotioncorrection;
int incrementaldeltay = mlasty != integer.min_value ? y - mlasty : deltay;
// no need to do all this work if we're not going to move
// anyway
boolean atedge = false;
if (incrementaldeltay != 0) {
atedge = trackmotionscroll(deltay, incrementaldeltay);
}
// check to see if we have bumped into the scroll limit
if (atedge && getchildcount() > 0) {
// treat this like we're starting a new scroll from the
// current
// position. this will let the user start scrolling back
// into
// content immediately rather than needing to scroll
// back to the
// point where they hit the limit first.
int motionposition = findmotionrow(y);
if (motionposition >= 0) {
final view motionview = getchildat(motionposition - mfirstposition);
mmotionvieworiginaltop = motionview.gettop();
}
mmotiony = y;
mmotionposition = motionposition;
invalidate();
}
mlasty = y;
}
break;
}
break;
}
case motionevent.action_up: {
switch (mtouchmode) {
case touch_mode_down:
case touch_mode_tap:
case touch_mode_done_waiting:
final int motionposition = mmotionposition;
final view child = getchildat(motionposition - mfirstposition);
if (child != null && !child.hasfocusable()) {
if (mtouchmode != touch_mode_down) {
child.setpressed(false);
}
if (mperformclick == null) {
mperformclick = new performclick();
}
final abslistview.performclick performclick = mperformclick;
performclick.mchild = child;
performclick.mclickmotionposition = motionposition;
performclick.rememberwindowattachcount();
mresurrecttoposition = motionposition;
if (mtouchmode == touch_mode_down || mtouchmode == touch_mode_tap) {
final handler handler = gethandler();
if (handler != null) {
handler.removecallbacks(mtouchmode == touch_mode_down ? mpendingcheckfortap
: mpendingcheckforlongpress);
}
mlayoutmode = layout_normal;
if (!mdatachanged && madapter.isenabled(motionposition)) {
mtouchmode = touch_mode_tap;
setselectedpositionint(mmotionposition);
layoutchildren();
child.setpressed(true);
positionselector(child);
setpressed(true);
if (mselector != null) {
drawable d = mselector.getcurrent();
if (d != null && d instanceof transitiondrawable) {
((transitiondrawable) d).resettransition();
}
}
postdelayed(new runnable() {
public void run() {
child.setpressed(false);
setpressed(false);
if (!mdatachanged) {
post(performclick);
}
mtouchmode = touch_mode_rest;
}
}, viewconfiguration.getpressedstateduration());
} else {
mtouchmode = touch_mode_rest;
}
return true;
} else if (!mdatachanged && madapter.isenabled(motionposition)) {
post(performclick);
}
}
mtouchmode = touch_mode_rest;
break;
case touch_mode_scroll:
final int childcount = getchildcount();
if (childcount > 0) {
if (mfirstposition == 0
&& getchildat(0).gettop() >= mlistpadding.top
&& mfirstposition + childcount < mitemcount
&& getchildat(childcount - 1).getbottom() <= getheight()
- mlistpadding.bottom) {
mtouchmode = touch_mode_rest;
reportscrollstatechange(onscrolllistener.scroll_state_idle);
} else {
final velocitytracker velocitytracker = mvelocitytracker;
velocitytracker.computecurrentvelocity(1000, mmaximumvelocity);
final int initialvelocity = (int) velocitytracker
.getyvelocity(mactivepointerid);
if (math.abs(initialvelocity) > mminimumvelocity) {
if (mflingrunnable == null) {
mflingrunnable = new flingrunnable();
}
reportscrollstatechange(onscrolllistener.scroll_state_fling);
mflingrunnable.start(-initialvelocity);
} else {
mtouchmode = touch_mode_rest;
reportscrollstatechange(onscrolllistener.scroll_state_idle);
}
}
} else {
mtouchmode = touch_mode_rest;
reportscrollstatechange(onscrolllistener.scroll_state_idle);
}
break;
}
setpressed(false);
// need to redraw since we probably aren't drawing the selector
// anymore
invalidate();
final handler handler = gethandler();
if (handler != null) {
handler.removecallbacks(mpendingcheckforlongpress);
}
if (mvelocitytracker != null) {
mvelocitytracker.recycle();
mvelocitytracker = null;
}
mactivepointerid = invalid_pointer;
if (profile_scrolling) {
if (mscrollprofilingstarted) {
debug.stopmethodtracing();
mscrollprofilingstarted = false;
}
}
break;
}
case motionevent.action_cancel: {
mtouchmode = touch_mode_rest;
setpressed(false);
view motionview = this.getchildat(mmotionposition - mfirstposition);
if (motionview != null) {
motionview.setpressed(false);
}
clearscrollingcache();
final handler handler = gethandler();
if (handler != null) {
handler.removecallbacks(mpendingcheckforlongpress);
}
if (mvelocitytracker != null) {
mvelocitytracker.recycle();
mvelocitytracker = null;
}
mactivepointerid = invalid_pointer;
break;
}
case motionevent.action_pointer_up: {
onsecondarypointerup(ev);
final int x = mmotionx;
final int y = mmotiony;
final int motionposition = pointtoposition(x, y);
if (motionposition >= 0) {
// remember where the motion event started
v = getchildat(motionposition - mfirstposition);
mmotionvieworiginaltop = v.gettop();
mmotionposition = motionposition;
}
mlasty = y;
break;
}
}
return true;
}
这个方法中的代码就非常多了,因为它所处理的逻辑也非常多,要监听各种各样的触屏事件。但是我们目前所关心的就只有手指在屏幕上滑动这一个事件而已,对应的是action_move这个动作,那么我们就只看这部分代码就可以了。
可以看到,action_move这个case里面又嵌套了一个switch语句,是根据当前的touchmode来选择的。那这里我可以直接告诉大家,当手指在屏幕上滑动时,touchmode是等于touch_mode_scroll这个值的,至于为什么那又要牵扯到另外的好几个方法,这里限于篇幅原因就不再展开讲解了,喜欢寻根究底的朋友们可以自己去源码里找一找原因。
这样的话,代码就应该会走到第78行的这个case里面去了,在这个case当中并没有什么太多需要注意的东西,唯一一点非常重要的就是第92行调用的trackmotionscroll()方法,相当于我们手指只要在屏幕上稍微有一点点移动,这个方法就会被调用,而如果是正常在屏幕上滑动的话,那么这个方法就会被调用很多次。那么我们进入到这个方法中瞧一瞧,代码如下所示:
boolean trackmotionscroll(int deltay, int incrementaldeltay) {
final int childcount = getchildcount();
if (childcount == 0) {
return true;
}
final int firsttop = getchildat(0).gettop();
final int lastbottom = getchildat(childcount - 1).getbottom();
final rect listpadding = mlistpadding;
final int spaceabove = listpadding.top - firsttop;
final int end = getheight() - listpadding.bottom;
final int spacebelow = lastbottom - end;
final int height = getheight() - getpaddingbottom() - getpaddingtop();
if (deltay < 0) {
deltay = math.max(-(height - 1), deltay);
} else {
deltay = math.min(height - 1, deltay);
}
if (incrementaldeltay < 0) {
incrementaldeltay = math.max(-(height - 1), incrementaldeltay);
} else {
incrementaldeltay = math.min(height - 1, incrementaldeltay);
}
final int firstposition = mfirstposition;
if (firstposition == 0 && firsttop >= listpadding.top && deltay >= 0) {
// don't need to move views down if the top of the first position
// is already visible
return true;
}
if (firstposition + childcount == mitemcount && lastbottom <= end && deltay <= 0) {
// don't need to move views up if the bottom of the last position
// is already visible
return true;
}
final boolean down = incrementaldeltay < 0;
final boolean intouchmode = isintouchmode();
if (intouchmode) {
hideselector();
}
final int headerviewscount = getheaderviewscount();
final int footerviewsstart = mitemcount - getfooterviewscount();
int start = 0;
int count = 0;
if (down) {
final int top = listpadding.top - incrementaldeltay;
for (int i = 0; i < childcount; i++) {
final view child = getchildat(i);
if (child.getbottom() >= top) {
break;
} else {
count++;
int position = firstposition + i;
if (position >= headerviewscount && position < footerviewsstart) {
mrecycler.addscrapview(child);
}
}
}
} else {
final int bottom = getheight() - listpadding.bottom - incrementaldeltay;
for (int i = childcount - 1; i >= 0; i--) {
final view child = getchildat(i);
if (child.gettop() <= bottom) {
break;
} else {
start = i;
count++;
int position = firstposition + i;
if (position >= headerviewscount && position < footerviewsstart) {
mrecycler.addscrapview(child);
}
}
}
}
mmotionviewnewtop = mmotionvieworiginaltop + deltay;
mblocklayoutrequests = true;
if (count > 0) {
detachviewsfromparent(start, count);
}
offsetchildrentopandbottom(incrementaldeltay);
if (down) {
mfirstposition += count;
}
invalidate();
final int absincrementaldeltay = math.abs(incrementaldeltay);
if (spaceabove < absincrementaldeltay || spacebelow < absincrementaldeltay) {
fillgap(down);
}
if (!intouchmode && mselectedposition != invalid_position) {
final int childindex = mselectedposition - mfirstposition;
if (childindex >= 0 && childindex < getchildcount()) {
positionselector(getchildat(childindex));
}
}
mblocklayoutrequests = false;
invokeonitemscrolllistener();
awakenscrollbars();
return false;
}
这个方法接收两个参数,deltay表示从手指按下时的位置到当前手指位置的距离,incrementaldeltay则表示据上次触发event事件手指在y方向上位置的改变量,那么其实我们就可以通过incrementaldeltay的正负值情况来判断用户是向上还是向下滑动的了。如第34行代码所示,如果incrementaldeltay小于0,说明是向下滑动,否则就是向上滑动。
下面将会进行一个边界值检测的过程,可以看到,从第43行开始,当listview向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子view,第47行当中,如果该子view的bottom值已经小于top值了,就说明这个子view已经移出屏幕了,所以会调用recyclebin的addscrapview()方法将这个view加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子view被移出了屏幕。那么如果是listview向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子view,然后判断该子view的top值是不是大于bottom值了,如果大于的话说明子view已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。
接下来在第76行,会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子view全部detach掉,在listview的概念当中,所有看不到的view就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证listview的高性能和高效率。紧接着在第78行调用了offsetchildrentopandbottom()方法,并将incrementaldeltay作为参数传入,这个方法的作用是让listview中所有的子view都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,listview的内容也会随着滚动的效果。
然后在第84行会进行判断,如果listview中最后一个view的底部已经移入了屏幕,或者listview中第一个view的顶部移入了屏幕,就会调用fillgap()方法,那么因此我们就可以猜出fillgap()方法是用来加载屏幕外数据的,进入到这个方法中瞧一瞧,如下所示:
/**
* fills the gap left open by a touch-scroll. during a touch scroll,
* children that remain on screen are shifted and the other ones are
* discarded. the role of this method is to fill the gap thus created by
* performing a partial layout in the empty space.
*
* @param down
* true if the scroll is going down, false if it is going up
*/
abstract void fillgap(boolean down);
ok,abslistview中的fillgap()是一个抽象方法,那么我们立刻就能够想到,它的具体实现肯定是在listview中完成的了。回到listview当中,fillgap()方法的代码如下所示:
void fillgap(boolean down) {
final int count = getchildcount();
if (down) {
final int startoffset = count > 0 ? getchildat(count - 1).getbottom() + mdividerheight :
getlistpaddingtop();
filldown(mfirstposition + count, startoffset);
correcttoohigh(getchildcount());
} else {
final int startoffset = count > 0 ? getchildat(0).gettop() - mdividerheight :
getheight() - getlistpaddingbottom();
fillup(mfirstposition - 1, startoffset);
correcttoolow(getchildcount());
}
}
down参数用于表示listview是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用filldown()方法,而如果是向上滑动的话就会调用fillup()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对listview进行填充,所以这两个方法我们就不看了,但是填充listview会通过调用makeandaddview()方法来完成,又是makeandaddview()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:
/**
* obtain the view and add it to our list of children. the view can be made
* fresh, converted from an unused view, or used as is if it was in the
* recycle bin.
*
* @param position logical position in the list
* @param y top or bottom edge of the view to add
* @param flow if flow is true, align top edge to y. if false, align bottom
* edge to y.
* @param childrenleft left edge where children should be positioned
* @param selected is this position selected?
* @return view that was added
*/
private view makeandaddview(int position, int y, boolean flow, int childrenleft,
boolean selected) {
view child;
if (!mdatachanged) {
// try to use an exsiting view for this position
child = mrecycler.getactiveview(position);
if (child != null) {
// 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;
}
不管怎么说,这里首先仍然是会尝试调用recyclebin的getactiveview()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次layout过程中我们已经从mactiveviews中获取过了数据,而根据recyclebin的机制,mactiveviews是不能够重复利用的,因此这里返回的值肯定是null。
既然getactiveview()方法返回的值是null,那么就还是会走到第28行的obtainview()方法当中,代码如下所示:
/**
* get a view and have it show the data associated with the specified
* position. this is called when we have already discovered that the view is
* not available for reuse in the recycle bin. the only choices left are
* converting an old view or making a new one.
*
* @param position
* the position to display
* @param isscrap
* array of at least 1 boolean, the first entry will become true
* if the returned view was taken from the scrap heap, false if
* otherwise.
*
* @return a view displaying the data associated with the specified position
*/
view obtainview(int position, boolean[] isscrap) {
isscrap[0] = false;
view scrapview;
scrapview = mrecycler.getscrapview(position);
view child;
if (scrapview != null) {
child = madapter.getview(position, scrapview, this);
if (child != scrapview) {
mrecycler.addscrapview(scrapview);
if (mcachecolorhint != 0) {
child.setdrawingcachebackgroundcolor(mcachecolorhint);
}
} else {
isscrap[0] = true;
dispatchfinishtemporarydetach(child);
}
} else {
child = madapter.getview(position, null, this);
if (mcachecolorhint != 0) {
child.setdrawingcachebackgroundcolor(mcachecolorhint);
}
}
return child;
}
这里在第19行会调用recylebin的getscrapview()方法来尝试从废弃缓存中获取一个view,那么废弃缓存有没有view呢?当然有,因为刚才在trackmotionscroll()方法中我们就已经看到了,一旦有任何子view被移出了屏幕,就会将它加入到废弃缓存中,而从obtainview()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取view。所以它们之间就形成了一个生产者和消费者的模式,那么listview神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,listview中的子view其实来来回回就那么几个,移出屏幕的子view会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现oom的情况,甚至内存都不会有所增加。
那么另外还有一点是需要大家留意的,这里获取到了一个scrapview,然后我们在第22行将它作为第二个参数传入到了adapter的getview()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getview()方法示例:
@override
public view getview(int position, view convertview, viewgroup parent) {
fruit fruit = getitem(position);
view view;
if (convertview == null) {
view = layoutinflater.from(getcontext()).inflate(resourceid, null);
} else {
view = convertview;
}
imageview fruitimage = (imageview) view.findviewbyid(r.id.fruit_image);
textview fruitname = (textview) view.findviewbyid(r.id.fruit_name);
fruitimage.setimageresource(fruit.getimageid());
fruitname.settext(fruit.getname());
return view;
}
第二个参数就是我们最熟悉的convertview呀,难怪平时我们在写getview()方法是要判断一下convertview是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertview,因为convertview就是我们之间利用过的view,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertview中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?
之后的代码又都是我们熟悉的流程了,从缓存中拿到子view之后再调用setupchild()方法将它重新attach到listview当中,因为缓存中的view也是之前从listview中detach掉的,这部分代码就不再重复进行分析了。
为了方便大家理解,这里我再附上一张图解说明:
那么到目前为止,我们就把listview的整个工作流程代码基本分析结束了,文章比较长,希望大家可以理解清楚