ListView的源码特别长,流程特别多,看完郭神的解析博客(
http://blog.csdn.net/guolin_blog/article/details/44996879)还是一脸懵逼。这篇文章根据郭神的文章,尝试记录下ListView源码的总体思路。
RecycleBin
首先ListView继承自AbsListView。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.
*
* @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
* @see android.widget.AbsListView.RecyclerListener
*/
可以知道这个类是用来帮助复用view的。这个类中有个保存当前在屏幕上显示的view的数组mActiveViews和可以被用来复用的view的mScrapViews:
/**
* 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;
在RecycleBin中提供了add/get/fill等方法用于对mActiveViews和mScrapViews进行操作,这里不列出。
第一次Layout
在View的工作流程中,已经知道onLayout方法是用于给ViewGroup确定子元素的位置的。ListView也是ViewGroup,但它没有重写onLayout方法,而是在其父类AbsListView中有实现。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
layoutChildren(); // 注1
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}
在onLayout方法中,前边是判断ListView的大小或位置有没有发生变化,如果发生变化就要求所有的子布局强制进行重绘。接下来看注1的layoutChildren方法。layoutChildren由ListView进行重写了,layoutChildren方法很长,我们直接看它的一个关键调用:
/**
* 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);
}
fillFromTop的任务就是从mFirstPosition开始,自顶至底去填充ListView。在这里又调用了fillDown()方法,填充ListView的操作都是在这里面进行的:
/**
* 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 = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= 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++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
在fillDown方法中,有个while循环来执行重复逻辑,填充ListView,直到nextTop(即元素的顶部坐标值)超过屏幕的底部了,或者pos大于mItemCount(即Adapter中所有的元素都被遍历了)时跳出循环。在while中,调用了makeAndAddView方法来获取一个View。
/**
* 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 existing 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;
}
在上面的makeAndAddView方法中,首先尝试从RecycleBin中获取一个active view。但第一次Layout时里面并没有缓存任何数据,得到的是null。然后调用obtainView获取View,obtainView的总体流程是,先用RecycleBin的getScrapView方法尝试获取一个废弃缓存中的View,如果没有,则调用mAdapter的getView方法,即是我们重写的getView方法来获取一个View,第一次Layout过程中,所有的View都是通过getView方法的inflate方法加载出来的,因此会比较耗时。获取到View之后就通过setupChild方法将View设置到ListView上。第一次layout至此结束。
第二次layout
第二次layout中,此时ListView中已有子View,因此可以保存到mActiveViews数组中。然后通过detachAllViewsFromParent方法将所有的子View清除,避免产生重复的数据。然后我们直接来看makeAndAddView方法。此时尝试从RecycleBin中获取active views一定可以获取到了,获取到后,直接setupChild,就不用再去通过getView方法inflate。
滑动加载更多数据
活动触摸监听在onTouchEvent中,具体的调用栈就不再去分析,太多也记不住。总体的处理逻辑是,当判断到子View的bottom/top值小于ListView的top/bottom值时,说明这个子View已经滑出屏幕,所以会调用RecycleBin的addScrapView方法将这个View加入到废弃缓存中,然后把所有移出屏幕的子View从ListView中detach掉。然后调用offsetChildrenTopAndBottom方法将ListView中的所有子View进行偏移,实现随着手指的移动,ListView跟着滚动的效果。
当有新的View移入屏幕时,就会尝试从RecycleBin中的getScrapView方法来从废弃缓存中获取一个View,然后调用getView方法,把获取到的废弃的View作为convertView参数传入,在getView方法中,我们就可以直接将convertView中的数据替换即可,免去inflate。