ListView 源码分析
Posted Chenantao_gg
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView 源码分析相关的知识,希望对你有一定的参考价值。
前言
虽然 RecyclerView 出来很长时间了,ListView 似乎已经过时了,但 ListView 仍然有许多优秀的思想值得学习。讲到 ListView,大家都会想到其复用机制,我这里就不废话说一大堆为什么需要复用等这些废话,直接进入正题。
源码分析
首先,由于 ListView 是个极其复杂的 View,由于本人能力以及篇幅的原因,不可能面面俱到的把整个 ListView 进行分析,那么这里我就分析最普遍的情况,以下就是我使用 ListView 的代码,我们将针对这段代码来进行分析。
mLv = (ListView) findViewById(R.id.lv);
List<String> datas = new ArrayList<>();
for (int i = 0; i < 30; i++) {
datas.add("xixi:" + i);
}
mLv.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, datas));
复用机制
在展开源代码之前,首先我们得对 ListView 的复用机制有一个了解,ListView 的复用逻辑主要由 ListView 的父类 AbsListView 的内部类 RecycleBin 来完成,这个内部类主要有如下属性:
- View[] mActiveViews = new View[0]:
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.
这个是官方的解释,我觉得很详细了,我在简单复述下,这个 mActiveViews
,主要在 layout 的时候使用,用于存储所有子 View。mActiveViews
里面的于元素只能够使用一次,一旦取出一个元素,那么对应的下标便被置为 null。
ArrayList[] mScrapViews;
这个就是复用机制的核心属性了,所有移出屏幕的 view 将会被存储在这个数组中。事实上,当 ViewType 只有一种的时候,废弃掉的 View 会存储在
private ArrayList<View> mCurrentScrap
中。再来看两个主要的方法:
void fillActiveViews(int childCount, int firstActivePosition)
这个方法我目前只找到在 ListView#layoutChildren 方法中调用,主要是将当前的所有子 View 填充到 mActiveViews 数组中。
void addScrapView(View scrap, int position)
这个看方法名就知道了,很明显,将 view 添加到废弃数组 mCurrentScrap 中。(这里假定 viewType 只有一种)
无论一个 View 如何复杂,说的多么玄乎,他终究都得经过 onMeasure 以及 onLayout 方法,那么我们就来从他的 onMeasure 方法入手。强调一下,这里我只研究复用机制,以下源代码我会剔除跟复用机制无关的代码,例如 dataChange、selectView 等部分代码。
## onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
//这里我们已经设置了 adapter,所以 mItemCount 的数量是不为0的。
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
// 一般情况下不会进入以下判断条件,但当父布局为 ScrollView 时,以下条件是成立的。
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
// 当测量模式为 UNSPECIFIED 时,ListView 的高度取第一个子 View 的高度。
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
//当测量模式为 wrap_content 时,会根据 itemCount 的数量来 inflate 子 view,最大高度不能超过 heightSize
if (heightMode == MeasureSpec.AT_MOST) {
//该方法接收四个参数,第 2、3 个参数是一个范围,表示使用 Adapter 哪个范围的数据。
//第四个参数为 ListView 的可用高度。
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
基于以上测量规则,可以解释两个现象。
- 当 ScrollView 嵌套 ListView 时,ListView 只显示第一项。
由于当 ListView 嵌套在 ScrollView 中时,高度的测量模式会被强制改为 UNSPECIFIED,则根据如下代码,ListView 的高度只能是第一项子 View 的高度。
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
为什么使用如下代码可以解决 ListView 嵌套在 ScrollView 中只显示一项的问题。
重写 ListView,在 onMeasure 方法中实现代码如下:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); super.onMeasure(widthMeasureSpec, expandSpec); }
这段是网上可以解决 ScrollView 嵌套 ListView 只显示第一项问题的代码,那么为什么可以解决呢?参考 ListView 如下代码实现。
if (heightMode == MeasureSpec.AT_MOST) { //该方法接收四个参数,第 2、3 个参数是一个范围,表示使用 Adapter 哪个范围的数据。 //第四个参数为 ListView 的可用高度。 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, Integer.MAX_VALUE >> 2, -1); }
我们把 Integer.MAX_VALUE >> 2 代入到代码中,大家都知道 MeasureSpec 为一个32位 int 值,高2位表示测量模式,后 30 位表示大小,那么 Integer.MAX_VALUE >> 2 即表示一个 View 最大可以多大。放到这个方法中则表示,adapter 中有多少 item,就加载多少 item。那么这样复用机制就一点卵用都没有了。
onMeasure 里面的一些方法我之所以不展开细说,是因为里面涉及到一部分的复用逻辑,等后面说清楚了后,在自己看,也不迟。同时,ListView 的复用逻辑,大部分还是在 onLayout 以及 onTouchEvent 中,onMeasure 并不是主要部分。
onLayout
在 ListView 中并没有找到 onLayout 这个方法,他存在于 ListView 的父类,AbsListView 中,在 onLayout 中,会调用 ListView 的 layoutChildren 方法,那么我们直接看 ListView#layoutChildren 方法就好。
layoutChidlren
由于 layoutChildren
方法偏长,这里我只保留跟复用逻辑有关的代码,且假定 itemCount 不为0.
@Override
protected void layoutChildren() {
try {
super.layoutChildren();
invalidate();
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount();
int index = 0;
int delta = 0;
// 需要记录上一次 layout 时的第一个子 view
//主要用到其 top 属性来设置本次 layout 第一个子 view 的开始位置
View oldFirst = null;
// Remember the previous first child
oldFirst = getChildAt(0);
//这个我们只分析其为 false 的情况。
boolean dataChanged = mDataChanged;
// 注意这个 position 不是 ListView 中 child 的下标,而是在 adapter 中的下标
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
// 这个会将当前所有子 View 存储到 recycleBin 的 mActiveViews 数组中。
recycleBin.fillActiveViews(childCount, firstPosition);
// 这个方法很重要,把所有 View 从 ListView 中 detach (移除)。
//因为后面涉及到对 ListView 的重新填充,如果没调用这个方法,那么就可能会有两份数据了。
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
// 这里就是填充 ListView 的方法了。
fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
// 这个我没仔细研究,不过粗略看下,应该是滚动到指定位置,跟复用逻辑无关
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
//填充结束后,将 mActiveViews 中的元素移除到 mScrapViews 中
recycleBin.scrapActiveViews();
//更新滚动条
updateScrollIndicators();
invokeOnItemScrollListener();
} finally {
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
上面的代码是不是看起来觉得 so easy? 事实上你自己去看 layoutChildren 方法,一开始绝对晕的想吐,这个是被我删除了很多无关代码后留下的。为了避免引起混淆,将我剔除的部分说明下。以上代码基于以下条件:
- ViewTypeCount 只有一种。
- itemCount > 0
- 忽略选择的的 item (setSelection)部分的逻辑
focus 以及一些我不懂的东西。
上面的代码一开始会将所有子 View 填充到
mActiveViews
数组中,然后进行填充ListView
。在填充ListView
结束后,会将mActiveViews
中剩余的元素移除到mScrapViews
数组中以供复用。recycleBin.scrapActiveViews();
注意这里强调剩余。虽然说是剩余,但是一般到这步的时候,mActiveViews
里的元素一般都被消费掉了,所以这一步好像也没啥卵用,有人知道mActiveViews
里有可用元素的情况的话,请留言,谢谢。那么 layoutChildren 方法就看完了,似乎没看到是如何进行填充 ListView 的,那么答案显而易见,肯定是在
fillSpecific()
方法中,那么我们跟进去。
fillSpecific()
/**
* 先根据 top 值填充指定位置 position 的 view,在填充两端的数据(往上和往下)
*
* @param position 先填充这个位置的 view,然后再填充这个 view 往上以及往下的数据
* @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;
//先根据 top 值填充第一个 view,再根据这个 view 填充两端的数据
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;
// 往上填充
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);
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
这个方法依会根据 top 值,往 ListView 中添加 item。但其实此时 position 传进来的是 firstPosition,top 也是第一个 view 的 top,所以并不会涉及到往上的填充,只会涉及到往下的填充。为了加深对这个方法的理解,我画了张图。这张图主要画了这个方法的逻辑,先根据 top 填充红色的 item,再填充红色 item 两端的数据。
那么由于此时往上填充(fillUp
) 虽然有调用,但却没进行填充,那么我们只需要分析往下填充(fillDown
) 就好。关于上面的 makeAndAddView ,下面进行分析。
fillDown()
private View fillDown(int pos, int nextTop) {
View selectedView = null;
// 得到 ListView 的高度
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
}
// nextTop 为要填充的 view 的 top 值,如果 top 大于 ListView 的高度或者 pos 已经大于 itemCount 了,则跳出循环。
// 每次循环都会累加 nextTop 以及 pos 的值。
// 这里也可以看到 ListView 为什么不会占用太大内存的原因,因为只会填充 ListView 高度大小的 View。
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;
}
上面的代码只展示了 ListView 如何填充 View,而具体如何获得一个 View 以及复用 View 还没有说明,那么点进去 makeAndAddView() 方法。
makeAndAddView
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {
View child;
if (!mDataChanged) {
// 尝试从 mActiveViews 中获取 view,如果当前在 layoutChildren,则此时有可能可以获得 view。
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;
}
}
// mActiveViews 没有 view,则调用 obtainView 方法获得一个 view,注意这个方法肯定会获得一个 view。
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
此方法作用如名字,制造一个 view(可能从缓存获得,也可能重新 inflate 获得)并且添加到 ListView。当 ListView 调用 layoutChildren 进行布局的时候,会调用recycleBin.fillActiveViews(childCount, firstPosition);
对 mActiveViews 进行填充,并且整个 ListView 中,也只有 layoutChildren 的时候会对 mActiveViews 进行填充。如果从mActiveViews
获取到 view,则直接调用setupChild()
对子 view 进行设置,比如位置等信息。如果获取不到,则调用obtainView(position, mIsScrap)
方法获取一个 view,这个方法肯定会获得一个 view,只不过可能是从废弃数组 mCurrenScraps 获得或者重新 inflate。获得 view 后,再调用setupChild()
对 view 进行设置。注意 setupChild()
的最后一个参数,这个参数决定是否对 view 进行 measure 以及 layout。
那么从上面看来,获取缓存 view 的逻辑就在这个 obtainView()
方法上了,赶紧跟进去看一下。
obtainView()
View obtainView(int position, boolean[] isScrap) {
//标识是否使用缓存的 view,将作为 setupChild 的最后一个参数传入
isScrap[0] = false;
//这里就是 ListView 的核心了,获取废弃的 view。
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
setItemViewLayoutParams(child, position);
return child;
}
上面的代码展示了 ListView 的核心,这也是我们重写 adapter 的 getView 方法时,为什么 converView 有时为空,有时不为空的原因了,关键就是 mRecycler.getScrapView(position)
是否为空,也就是是否有废弃的 view。这里注意 isScrap[0],该方法标明是否有使用废弃的 view,将决定后续是否对 view 进行 measure 以及 layout。
那么到这里,我们已经大概了解了 ListView 如何得到一个 view,以及它的复用机制,那么搂一眼获得 view 之后对 view 会进行怎样的操作。回到makeAndAddView()
方法,找到 setupChild()
方法,跟进去。
setupChild()
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
//如果 child 是从缓存(mActiveViews 或者 mCurrentScrape)中获得,则 recycled 为 true。
//如果 recycled 为 false,则证明 view 是重新 inflate 出来的,则 needToMeasure 为 true。
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 = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
//如果是从缓存中获得的 view,则重新将其与 parent(ListView) 进行关联即可
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
//如果是重新 inflate 出来的 view,则添加到 layout 中,其内部同样会使其跟 ListView 进行关联
p.forceAdd = false;
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
// child 为重新 inflate 出来的,需要重新 measure
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec = View.MeasureSpec.makeMeasureSpec(lpHeight, View.MeasureSpec.EXACTLY);
} else {
childHeightSpec = View.MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
View.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;
//child 为重新 inflate 出来的,需要重新 layout
if (needToMeasure) {
final int childRight = childrenLeft + w;
final int childBottom = childTop + h;
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
// 如果是从缓存中获取的 view,则代表此 view 已经添加到布局中了,将其偏移到新位置即可。
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
}
setupChild 主要是根据 view 是否从缓存中获取的来执行相应的逻辑。注意到attachViewToParent()
方法以及addViewInLayout()
方法。个人能力有限,且网上资料也没过多说明,仅从官方注释来解释下这两个方法。attachViewToParent()
内部其实没有对 view 进行添加的代码,而仅仅是使 view 的 parent 的属性指向了父布局,也就是说,只是完成了一个关联关系而已,从而使得可以通过 viewGroup.getChildAt(index)
来获得这个 view。
而addViewInLayout()
方法,其内部会调用addViewInner()
来添加 view 以及关联 parent ,平时我们调用viewGroup.addView()
的时候,其实到后面也会调用addViewInner()
方法。
那么我的个人猜测就是,attachViewToParent()
仅仅只是完成了关联,并没有真正的把 view 添加到父布局中。而addViewInLayout()
,是真真正正的把 view 添加到 layout 中。
那么结合上面代码,假设 recycled 为 true,那么 child 是从缓存中获得,也就是说,child 之前已经完成过 添加到 layout addViewInLayout()
操作,那么我们要做的仅仅就只是重新关联一遍即可。
同时后面源码我们可以看到,当 view 移出屏幕,添加到废弃 view 数组 mCurrentScraps
中时,也并没有调用 removeView()
将 view 从布局中移除,而仅仅是调用detachViewsFromParent()
解除关联关系而已。
我们可以(意)假(淫)设一下这样设计的好处。由于 ListView 的所有子 View 只会是显示在屏幕中的 item,所以当 item 移出屏幕时,肯定不能让其继续存在于 parent 中,但是 addView 以及 removeView 又是个相对较重的操作,那么采用关联这种操作,将会对性能的提升有一定的好处。
好了,ListView 的复用机制我们已经分析的七七八八了,结合以下,我们可以对接下来 ListView 最牛逼的部分进行分析了,那就是滑动时的复用机制。
说到滑动,那肯定存在于 onTouchEvent#move
中了,不过这种机制属于 GridView 以及 ListView 共有的,所以写在其父类 AbsListView
中。onTouchEvent 关于 move 的操作只是调用了onTouchMove()
,那么我们直接看这个方法。
onTouchMove()
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
pointerIndex = 0;
mActivePointerId = ev.getPointerId(pointerIndex);
}
final int y = (int) ev.getY(pointerIndex);
scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
}
事实上,onTouchMove()
方法是有一堆 switch case 判断的,我们简单起见,只走正常流程。跟进scrollIfNeeded()
方法。
scrollIfNeeded()
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
//总共滑动了多少
int rawDeltaY = y - mMotionY;
int scrollOffsetCorrection = 0;
int scrollConsumedCorrection = 0;
if (mLastY == Integer.MIN_VALUE) {
rawDeltaY -= mMotionCorrection;
}
final int deltaY = rawDeltaY;
//每个 event 产生时相比上个 event 时的偏移量。
//rawDeltaY 指的是一个触摸周期的偏移
//incrementalDeltaY 是一个 event 周期的偏移
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
int lastYCorrection = 0;
//这里是有分支的,我删除了,因为滑动的时候是会进入这个分支
//并且这里有复用的逻辑
if (mTouchMode == TOUCH_MODE_SCROLL) {
if (y != mLastY) {
// We may be here after stopping a fling and continuing to scroll.
// If so, we haven‘t disallowed intercepting touch events yet.
// Make sure that we do so in case we‘re in a parent that can intercept.
if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
Math.abs(rawDeltaY) > mTouchSlop) {
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
// 事件触发在 ListView items 的下标
final int motionIndex;
//mMotionPosition 是触摸事件在哪个 item 的位置上, 在 down 的时候完成赋值
if (mMotionPosition >= 0) {
motionIndex = mMotionPosition - mFirstPosition;
} else {
// If we don‘t have a motion position that we can reliably track,
// pick something in the middle to make a best guess at things below.
motionIndex = getChildCount() / 2;
}
int motionViewPrevTop = 0;
View motionView = this.getChildAt(motionIndex);
if (motionView != null) {
motionViewPrevTop = motionView.getTop();
}
// No need to do all this work if we‘re not going to move anyway
//是否到达了边界,即不能上滑或下滑
boolean atEdge = false;
if (incrementalDeltaY != 0) {
//检测滑动,并回收 view 以及复用 view
atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
}
// Check to see if we have bumped into the scroll limit
motionView = this.getChildAt(motionIndex);
if (motionView != null) {
// Check if the top of the motion view is where it is
// supposed to be
final int motionViewRealTop = motionView.getTop();
mLastY = y + lastYCorrection + scrollOffsetCorrection;
}
}
}
}
scrollIfNeeded()
方法的代码非常长,我删除了很多,例如到达边界以及嵌套滚动这些逻辑,如果一起分析的话实在太乱了。
上面的代码要知道变量是什么意思,然后主要看 trackMotionScroll(deltaY, incrementalDeltaY)
方法就好。这个方法里面就包含了回收 view 以及复用 view,在滑动回收的核心。那么我们跟进去。
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;
// "effective padding" In this case is the amount of padding that affects
// how much space should not be filled by items. If we don‘t clip to padding
// there is no effective padding.
int effectivePaddingTop = 0;
int effectivePaddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
effectivePaddingTop = listPadding.top;
effectivePaddingBottom = listPadding.bottom;
}
//第一个 item 超出屏幕的空间
final int spaceAbove = effectivePaddingTop - firstTop;
final int end = getHeight() - effectivePaddingBottom;
//最后一个 item 超出屏幕的空间
final int spaceBelow = lastBottom - end;
final int height = getHeight() - mPaddingBottom - mPaddingTop;
final int firstPosition = mFirstPosition;
final boolean cannotScrollDown = (firstPosition == 0 &&
firstTop >= listPadding.top && incrementalDeltaY >= 0);
final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
//如果不能上滑或者下滑,返回 true 代表已到达边界
if (cannotScrollDown || cannotScrollUp) {
return incrementalDeltaY != 0;
}
//是否上滑
final boolean down = incrementalDeltaY < 0;
int start = 0;
int count = 0;
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
//这里由于是上滑,所以当 child view 的 bottom 大于 top,证明还没有 view 滑出顶部,不做处理。
if (child.getBottom() >= top) {
break;
} else {
//count 统计总共回收的 view 个数
count++;
int position = firstPosition + i;
//只回收非 headerView 以及 非footerView
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
//添加到废弃 view
mRecycler.addScrapView(child, position);
}
}
}
}
//mMotionViewOriginalTop 是触摸到的 item 的 top 值,在 down 的时候完成赋值,
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
//有回收掉的 view,解除关联关系,且由于是下滑,所以 start 肯定是0
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
// 滑动 view,跟 scrollTo 作用差不多
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
//当第一个 view 超出屏幕的部分大于滑动的偏移量或者最后一个 view 超出屏幕的大小小于滑动的偏移量,填充 ListView
//这个判断我也看不懂
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
// 填充 ListView
fillGap(down);
}
invokeOnItemScrollListener();
return false;
}
这段代码我同样进行删除,只保留上滑部分的代码。可以看到内部会遍历子 view,当 view 的 bottom 小于顶部 top 时候,就会回收 view。同时会把回收掉的 view 跟父布局解除关联关系detachViewsFromParent()
。同时 ListView 的滑动效果也是在里面实现的,offsetChildrenTopAndBottom()
。然后最后,填充 ListView,调用fillGap(down)
,当上滑的时候,fillGap()
内部会调用 fillDown()
方法。不过他并不会从第一个 view 开始填充,而是从最后一个 view 开始像下填充,直到填满 ListView 。我们简单看一眼。
fillGap()
@Override
void fillGap(boolean down) {
final int count = getChildCount();
//上滑
if (down) {
int paddingTop = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingTop = getListPaddingTop();
}
//偏移量从最后一个 view 的底部开始,将作为 fillDown 开始填充的初始位置
final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
paddingTop;
fillDown(mFirstPosition + count, startOffset);
correctTooHigh(getChildCount());
} else {
int paddingBottom = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
paddingBottom = getListPaddingBottom();
}
final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
getHeight() - paddingBottom;
fillUp(mFirstPosition - 1, startOffset);
correctTooLow(getChildCount());
}
}
总结
再把 ListView 的流程过一遍。
layout 部分
首先,ListView 的子 View 是在 layoutChildren 方法中进行添加的。在 layoutChildren 的时候,会把当前已有的子 View 填充到 mActiveViews 中,然后把所有的子 View 跟 ListView 解除关联关系,因为后面填充的时候会再次关联。
第一次 layoutChildren 的时候,由于没有子 view,所以这时候会回调 adapter 的 getView 方法,获得相应数量的 子 view。
第二次 layoutChildren 的时候,此时已经有子 view 了,所以会填充到 mActiveViews,接着填充的时候会使用 mActiveViews 里面的 view,而不会重新 inflate。
ListView 进行 layoutChildren 的时候,基本是不会涉及到添加到废弃 view 数组 mCurrenScrap。
滑动部分
当 view 滑出屏幕的时候,不是调用 removeView,而是调用 detachViewsFromParent 解除关联关系。下次使用的时候,直接与 ListView 进行关联,然后进行相应位置的偏移即可。
剩下的可以结合这个流程自己再去过源码。
ListView 是个相对庞大的 view,学习其源码相对较困难,我自己也是看了好几天才梳理出来这逻辑,下一篇将自己写个 ListView 来加深印象,敬请期待。
以上是关于ListView 源码分析的主要内容,如果未能解决你的问题,请参考以下文章
从源码上分析ListView的addHeaderView和setAdapter的调用顺序
Android 5.1 Contacts源码分析:Contacts模块ListView Adapter结构