ListView相信很多读者对这个控件并不陌生,因为我们很多应用程序都是用到了这个控件,而这个控件到工作原理相信不少读者并不是很清楚,本文就由浅入深对这个控件进行详细的解释。
在正式介绍ListView之前,我们需要先了解Adapter(适配器)这个机制。
Adapter相信很多人对这个并不陌生,因为我们平时使用ListView时跟Adapter密不可分。那么为什么ListView这个控件需要使用Adapter这个适配器呢? 我们不妨细细思考一下,控件的基本目的是为了将数据展示在屏幕上,能够与用户进行交互,当然ListView这个控件也不例外。试想一下如果没有Adapter这个适配器,ListView如何将数据展示在屏幕上呢?可能有读者会想到,为什么不能将ListView设计的如同TextView一样,直接通过setXXX方法将数据展示出来呢,当然这样或许是可以的,但是有一点是你是否想过我们面临的数据源是千变万化的,如果这样设计,那么是否是需要设计针对不同数据源的不同的ListView呢?如果这样设计 一:程序的扩展性将大大降低,二ListView代码将会变的非常臃肿。这显然是我们不愿意看到的。针对这个问题,google的android开发工程师设计了Adapter这种机制来解决这个问题。Adapter顾名思义--适配器 它是ListView与数据源之间的桥梁,ListView不会与数据源直接打交道,而是通过Adapter这个桥梁去访问真正的数据,同时在android api中我们可以看到google对外提供了一个统一的借口BaseAdapter,我们可以通过实现该接口然后通过实现接口内的各种方法,去完成对不同数据源的适配。
下面我们通过简单的图示来说明adapter、listview、数据源之间的关系
下面我们来讲述外部数据源是如何通过adapter展示在ListView上的;
无论是系统提供的Adapter还是我们自己定义的Adapter 其根本都是实现了BaseAdapter,那么我们来看看BaseAdapter究竟是什么?
在android api文档中我们可以看到
BaseAdapter是一个抽象类,继承了Object同时 实现 了ListAdapter、SpinnerAdapter
而ListAdapter与SpinnerAdapter同时实现了Adapter 接口
在BaseAdapter中我们可以看到内部的具体方法
ListAdapter中的内部具体方法是:
SpinnerAdapter的内部具体方法是:
针对上述抽象类与接口中的方法似乎并没有发现我们常用的几个方法,那么我们继续看Adapter接口
很明显在Adapter接口中我们看到了我们常用的几个方法了,下面我们对以下我们常用的这几个方法进行详细讲解,以便读者能够熟练的掌握自定义Adapter
getCount():此适配器表示的数据集中有多少个项目;
getItem():获取与数据集中指定位置相关联的数据项;
getItemId():获取列表中指定位置关联的行标识;
getView():获取一个视图,该视图在数据集的指定位置显示数据;
getItemViewType(): 创建一个特殊的item之后获取这个item视图的类型;
getViewTypeCount():获取创建的视图view的数量;
getDropDownView():获取下拉的视图View
ok~~ 到这我们对adapter中各个方法应该有了初步的了解,那么这些方法具体是在什么时候调用的我们还不清楚。
针对上述问题,我们通过分析ListView的内部实现可以对adapter中这些方法有更深一步的了解,下面我们就开始具体分析ListView中的实现原理。
我们都知道ListView通过setAdapter(xxx)方法来实现适配器与ListView的连接,那么我们就从setAdapter方法开始,一步一步详细分析ListView的实现机制。
在setAdapter方法中可以看到:
@Override
public void setAdapter(ListAdapter adapter) {
...
if (mAdapter != null) {
...
mOldItemCount = mItemCount;
mItemCount = mAdapter.getCount();
checkFocus();
....
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()); ....
requestLayout();
}
内部通过 mOldItemCount 该变量记录了Adapter中 数据集中的个数,同时通过
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount())
来设置Adapter viewType的个数。之后调用
requestLayout();
方法来绘制当前视图
而该方法的具体实现我们可以通过内部源码看到是执行到了
AbsListView该抽象类中的requestLayout方法;
@Override
public void requestLayout() {
if (!mBlockLayoutRequests && !mInLayout) {
super.requestLayout();
}
}
在上述方法中实际是调用到了View类中的requestLayout
在View的requestLayout方法中;
@CallSuper
public void requestLayout() {
...
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
....
}
在View的requestLayout中我们发现requestLayout()任务上抛至其mParent
mParent.requestLayout()
mParent我们在代码中发现是 ViewParent 类型,而mParent类型我们通过源码可以知道实际是一个接口。因此我们需要找到实现requestLayout方法的实现类。
我们通过源码知道 ViewRootImpl是ViewParent的实现类,那么我们可以看该类中是否有requestLayout方法的实现。 功夫不负有心人
在ViewRootImpl中存在该方法的实现:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
通过 scheduleTraversals() 该方法 开始对View进行绘制,而View的绘制无非是三步流程 onMeasure()用于测量View的大小,onLayout()用于确定View的布局,onDraw()用于将View绘制到界面而对于ListView而言,onMeasure()并没有什么特殊的地方,因为它终归是一个View,占用的空间最多并且通常也就是整个屏幕。onDraw()在ListView当中也没有什么意义,因为ListView本身并不负责绘制,而是由ListView当中的子元素来进行绘制的。那么ListView大部分的神奇功能其实都是在onLayout()方法中进行的了
下面我们主要来看ListView的onLayout方法;
而我们在ListView中是没有找到改方法,ok 我们只能通过ListView的超类AbsListView中看看是否有该方法的实现,在AbsListView中我们看到:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
...
layoutChildren();
...
}
而该方法在AbsListView中是空实现
protected void layoutChildren() {
}
由此我们可以看到ListView控件的主要绘制实在AbsListView中进行绘制的,而对于ListView中子View的绘制 需要Listview控件去具体实现。现在我们具体去看ListView中 layoutChildren 的具体实现
@Override
protected void layoutChildren() {
...
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
//我们知道 mLayoutMode默认模式为 LAYOUT_NORMAL
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
...
break;
case LAYOUT_SYNC:
...
break;
case LAYOUT_FORCE_BOTTOM:
...
break;
case LAYOUT_FORCE_TOP:
...
break;
case LAYOUT_SPECIFIC:
...
break;
case LAYOUT_MOVE_SELECTION:
...
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;
}
.....
在这段代码中我们着重关注改方法 ;
fillFromTop(childrenTop);
该方法主要 负责的主要任务就是从mFirstPosition开始,自顶至底去填充ListView
接下来我们看一下fullFromTop内部具体是实现了那些;
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);
}
在源码中我们可以看懂内部主要是调用了 fillDown该方法,因此我们大致猜测 ListView Item的填充是在fillDown该方法内实现的。
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) {
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;
}
在代码内部我们可以看到 内部使用了一个while循环来执行重复的逻辑一开始nextTop的值是第一个子元素顶部距离整个ListView顶部的像素值,pos则是刚刚传入的mFirstPosition的值,而end是ListView底部减去顶部所得的像素值,mItemCount则是Adapter中的元素数量。因此一开始的情况下nextTop必定是小于end值的,并且pos也是小于mItemCount值的。那么每执行一次while循环,pos的值都会加1,并且nextTop也会增加,当nextTop大于等于end时,也就是子元素已经超出当前屏幕了,或者pos大于等于mItemCount时,也就是所有Adapter中的元素都被遍历结束了,就会跳出while循环。
现在我们着重留意 makeAndAddView 方法
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
针对于该方法内部 我们着重 跟踪 obtainView方法内部是如何实现的。
在AbsListView类中我们找到了该方法 ,改方法内部的具体实现我们可以看到是:
View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[0] = false;
final View transientView = mRecycler.getTransientStateView(position);
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
if (params.viewType == mAdapter.getItemViewType(position)) {
final View updatedView = mAdapter.getView(position, transientView, this);
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position);
mRecycler.addScrapView(updatedView, position);
}
}
isScrap[0] = true;
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
isScrap[0] = true;
child.dispatchFinishTemporaryDetach();
}
}
....
return child;
}
在这段代码中我们可以重点关注
final View child = mAdapter.getView(position, scrapView, this);
该行代码,可以看出 mAdapter的getView()方法来去获取一个View 而getView 就是我们平时使用ListView时最最经常重写的一个方法了.
在 makeAndAddView 方法执行完 obtainView 方法之后,然后调用了;
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
在setupChild方法内部我们可以看到
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean recycled) {
...
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
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 (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 = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
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());
}
....
}
内部我们看到 通过 addViewInLayout()方法将它添加到了ListView当中。那么根据fillDown()方法中的while循环,会让子元素View将整个ListView控件填满然后就跳出,也就是说即使我们的Adapter中有一千条数据,ListView也只会加载第一屏的数据,剩下的数据反正目前在屏幕上也看不到,所以不会去做多余的加载工作,这样就可以保证ListView中的内容能够迅速展示到屏幕上
ok ~~ 到这里我们大致了解了Adapter、ListView、数据源之间是如何相互联系的。但是此时我们是否存在一个疑问呢? 如果数据源是几千甚至上万条呢?ListView中是否会建立几千甚至上万条view呢?很显然google 是不会这么做的。
在源代码中我们找到了google 解决这个问题的机制 ---RecycleBin机制
该类是位于AbsListView的一个内部类,其源代码我们可以看到包含了一下方法:
class RecycleBin {
....
public void setViewTypeCount(int viewTypeCount) {
.....
}
public void markChildrenDirty() {
.....
}
public boolean shouldRecycleViewType(int viewType) {
...
}
void clear() {
...
}
void fillActiveViews(int childCount, int firstActivePosition) {
....
}
View getActiveView(int position) {
...
}
View getTransientStateView(int position) {
....
}
void clearTransientStateViews() {
...
}
View getScrapView(int position) {
...
}
void addScrapView(View scrap, int position) {
...
}
private ArrayList getSkippedScrap() {
...
}
void removeSkippedScrap() {
...
}
void scrapActiveViews() {
...
}
private void pruneScrapViews() {
...
}
void reclaimScrapViews(List views) {
...
}
void setCacheColorHint(int color) {
....
}
private View retrieveFromScrap(ArrayList scrapViews, int position) {
....
}
private void clearScrap(final ArrayList scrap) {
...
}
private void clearAccessibilityFromScrap(View view) {
...
}
private void removeDetachedView(View child, boolean animate) {
...
}
}
我们先来对这几个方法进行简单解读,这对后面分析ListView的工作原理将会有很大的帮助。
void fillActiveViews(int childCount, int firstActivePosition) {
....
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
final View[] activeViews = mActiveViews;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
fillActiveViews() 这个方法接收两个参数,第一个参数表示要存储的view的数量,第二个参数表示ListView中第一个可见元素的position值。RecycleBin当中使用mActiveViews这个数组来存储View,调用这个方法后就会根据传入的参数来将ListView中的指定元素存储到mActiveViews数组当中。
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;
}
getActiveView() 这个方法和fillActiveViews()是对应的,用于从mActiveViews数组当中获取数据。该方法接收一个position参数,表示元素在ListView当中的位置,方法内部会自动将position值转换成mActiveViews数组对应的下标值。需要注意的是,mActiveViews当中所存储的View,一旦被获取了之后就会从mActiveViews当中移除,下次获取同样位置的View将会返回null,也就是说mActiveViews不能被重复利用。
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
scrap.dispatchStartTemporaryDetach();
notifyViewAccessibilityStateChangedIfNeeded(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<>();
}
mTransientStateViewsById.put(lp.itemId, scrap);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<>();
}
mTransientStateViews.put(position, scrap);
} else {
getSkippedScrap().add(scrap);
}
} else {
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
addScrapView() 用于将一个废弃的View进行缓存,该方法接收一个View参数,当有某个View确定要废弃掉的时候(比如滚动出了屏幕),就应该调用这个方法来对View进行缓存,RecycleBin当中使用mScrapViews和mCurrentScrap这两个List来存储废弃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;
}
getScrapView 用于从废弃缓存中取出一个View,这些废弃缓存中的View是没有顺序可言的,因此getScrapView()方法中的算法也非常简单,就是直接从mCurrentScrap当中获取尾部的一个scrap view进行返回。
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
ArrayList[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList();
}
mViewTypeCount = viewTypeCount;
mCurrentScrap = scrapViews[0];
mScrapViews = scrapViews;
}
setViewTypeCount() 我们都知道Adapter当中可以重写一个getViewTypeCount()来表示ListView中有几种类型的数据项,而setViewTypeCount()方法的作用就是为每种类型的数据项都单独启用一个RecycleBin缓存机制。实际上,getViewTypeCount()方法通常情况下使用的并不是很多,所以我们只要知道RecycleBin当中有这样一个功能就行了。
下面我们从ListView Layout开始进行分析 RecycleBin 机制是如何实现的。
通过上文的分析我们知道ListView 子元素布局的具体实现是在 ListView的 layoutChildren()方法中。
我们知道第一次layout时 ListView是没有任何子view的 所以我们可以知道 layoutChildren()方法中
int childCount = getChildCount();
childCount肯定为0;
接着我们看 :
if (dataChanged) {
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition);
}
而dataChanged是在数据发生变化时才会为true 其他情况下为false,所以第一个layout我们可以断定 调用了
recycleBin.fillActiveViews(childCount, firstPosition)
该方法 fillActiveViews()是为了将ListView的子View进行缓存的,可是目前ListView中还没有任何的子View,因此这一行暂时还起不了任何作用
紧接着
switch (mLayoutMode) {....}
mLayoutMode的值来决定布局模式,默认情况下都是普通模式LAYOUT_NORMAL,这样会执行
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop);
} else {
...
}
} else {
...
}
下面又会紧接着进行两次if判断,childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会调用到fillFromTop()方法
而对于fillFromTop我们上述已经说过,其本质内部调用了fillDown 方法 而对于fillDown方法 其内部 无非是一个while 循环,循环体内部主要是调用了 makeAndAddView 方法,紧接着在 makeAndAddView 方法内部我们看到;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
内部执行首先 尝试从RecycleBin当中快速获取一个activeview 如果该view 不为null 则使用该view,如果为null 则会去 obtainView 一个view 然后通过调用setupChild来将该View显示在屏幕上。
上述讲述到是第一次layout的基本过程,对于非第一次layout 读者可以自行通过源码进行分析,其基本过程还是通过ListView的 layoutChildren()开始,最终还是通过执行 setupChild方法将view显示在屏幕上。
接下来我们来看ListView 是如何通过滑动来加载更多的数据的,通过对ListView的滑动解析,我们将会更清楚的了解RecycleBin 该机制在ListView中是如何使用的。
首先ListView的滑动实现是在 onTouchEvent 中实现的,所以我们去ListView该类中去找该方法的实现。
很遗憾我们并没有在ListView中找到该方法,我们仔细想想,该方法其实是一个通用型方法,所以我想该方法应该是在AbsListView中实现的。
@Override
public boolean onTouchEvent(MotionEvent ev) {
....
switch (actionMasked) {
case MotionEvent.ACTION_DOWN: {
onTouchDown(ev);
break;
}
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
case MotionEvent.ACTION_UP: {
onTouchUp(ev);
break;
}
case MotionEvent.ACTION_CANCEL: {
onTouchCancel();
break;
}
case MotionEvent.ACTION_POINTER_UP: {
...
break;
}
}
return true;
}
在该方法中我们只关心其内部 ACTION_MOVE 这个动作即可。
在ACTION_MOVE 中我们看到实质上是调用了onTouchMove 方法
case MotionEvent.ACTION_MOVE: {
onTouchMove(ev, vtev);
break;
}
在 onTouchMove方法中 内部实现:
private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
....
final int y = (int) ev.getY(pointerIndex);
switch (mTouchMode) {
....
if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
break;
}
....
}
我们可以看到 内部首先调用了
startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)
该方法,而在改方法内部:
private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
....
scrollIfNeeded(x, y, vtev);
return true;
}
调用了 scrollIfNeeded(x, y, vtev) 方法,在 scrollIfNeeded(x, y, vtev);方法内部 在incrementalDeltaY 不为0 的情况下
if (inc
atEdge = trackMotionScroll(deltaY, inc
}
内部调用到了 trackMotionScroll 该方法
*/
boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
....
if (incrementalDeltaY < 0) {
incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
} else {
incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
}
.....
if (down) {
...
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
...
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
...
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;
...
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
}
.....
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
....
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
//ListView中最后一个View的底部已经移入了屏幕 或者第一个view顶部移入了屏幕
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
.....
}
这个方法接收两个参数,deltaY表示从手指按下时的位置到当前手指位置的距离,incrementalDeltaY则表示据上次触发event事件手指在Y方向上位置的改变量,那么其实我们就可以通过incrementalDeltaY的正负值情况来判断用户是向上还是向下滑动的了
下面将会进行一个边界值检测的过程,可以看到,当ListView向下滑动的时候,就会进入一个for循环当中,从上往下依次获取子View,如果该子View的bottom值已经小于top值了,就说明这个子View已经移出屏幕了,所以会调用RecycleBin的addScrapView()方法将这个View加入到废弃缓存当中,并将count计数器加1,计数器用于记录有多少个子View被移出了屏幕。那么如果是ListView向上滑动的话,其实过程是基本相同的,只不过变成了从下往上依次获取子View,然后判断该子View的top值是不是大于bottom值了,如果大于的话说明子View已经移出了屏幕,同样把它加入到废弃缓存中,并将计数器加1。
接下来会根据当前计数器的值来进行一个detach操作,它的作用就是把所有移出屏幕的子View全部detach掉,在ListView的概念当中,所有看不到的View就没有必要为它进行保存,因为屏幕外还有成百上千条数据等着显示呢,一个好的回收策略才能保证ListView的高性能和高效率。紧接着在调用了offsetChildrenTopAndBottom()方法,并将incrementalDeltaY作为参数传入,这个方法的作用是让ListView中所有的子View都按照传入的参数值进行相应的偏移,这样就实现了随着手指的拖动,ListView的内容也会随着滚动的效果。
然后进行判断,如果ListView中最后一个View的底部已经移入了屏幕,或者ListView中第一个View的顶部移入了屏幕,就会调用fillGap()方法,那么因此我们就可以猜出fillGap()方法是用来加载屏幕外数据的
而fillGap我们在抽象类AbsListView中看到是一个没有实现的方法
abstract void fillGap(boolean down);
这样我们可以从ListView中看fillGap的具体实现是怎么样的
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();
}
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());
}
}
down参数用于表示ListView是向下滑动还是向上滑动的,可以看到,如果是向下滑动的话就会调用fillDown()方法,而如果是向上滑动的话就会调用fillUp()方法。那么这两个方法我们都已经非常熟悉了,内部都是通过一个循环来去对ListView进行填充,所以这两个方法我们就不看了,但是填充ListView会通过调用makeAndAddView()方法来完成,又是makeAndAddView()方法,但这次的逻辑再次不同了,所以我们还是回到这个方法瞧一瞧:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
child = mRecycler.getActiveView(position);
if (child != null) {
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
child = obtainView(position, mIsScrap);
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
不管怎么说,这里首先仍然是会尝试调用RecycleBin的getActiveView()方法来获取子布局,只不过肯定是获取不到的了,因为在第二次Layout过程中我们已经从mActiveViews中获取过了数据,而根据RecycleBin的机制,mActiveViews是不能够重复利用的,因此这里返回的值肯定是null。
而在 obtainView 该方法中
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);
...
} else {
isScrap[0] = true;
dispatchFinishTemporaryDetach(child);
}
} else {
child = mAdapter.getView(position, null, this);
...
}
return child;
}
调用RecyleBin的getScrapView()方法来尝试从废弃缓存中获取一个View,那么废弃缓存有没有View呢?当然有,因为刚才在trackMotionScroll()方法中我们就已经看到了,一旦有任何子View被移出了屏幕,就会将它加入到废弃缓存中,而从obtainView()方法中的逻辑来看,一旦有新的数据需要显示到屏幕上,就会尝试从废弃缓存中获取View。所以它们之间就形成了一个生产者和消费者的模式,那么ListView神奇的地方也就在这里体现出来了,不管你有任意多条数据需要显示,ListView中的子View其实来来回回就那么几个,移出屏幕的子View会很快被移入屏幕的数据重新利用起来,因而不管我们加载多少数据都不会出现OOM的情况,甚至内存都不会有所增加。
那么另外还有一点是需要大家留意的,这里获取到了一个scrapView,然后我们在将它作为第二个参数传入到了Adapter的getView()方法当中。那么第二个参数是什么意思呢?我们再次看一下一个简单的getView()方法示例
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
return view;
}
第二个参数就是我们最熟悉的convertView呀,难怪平时我们在写getView()方法是要判断一下convertView是不是等于null,如果等于null才调用inflate()方法来加载布局,不等于null就可以直接利用convertView,因为convertView就是我们之间利用过的View,只不过被移出屏幕后进入到了废弃缓存中,现在又重新拿出来使用而已。然后我们只需要把convertView中的数据更新成当前位置上应该显示的数据,那么看起来就好像是全新加载出来的一个布局一样,这背后的道理你是不是已经完全搞明白了?
之后的代码又都是我们熟悉的流程了,从缓存中拿到子View之后再调用setupChild()方法将它重新attach到ListView当中,因为缓存中的View也是之前从ListView中detach掉的,这部分代码就不再重复进行分析了。
下面我们通过一张图来说明 :