ListView是一个ViewGroup,对于ListView的使用需要分为两个部分,一个是ListView本身,二是adapter,他们各自的作用也是很分明的,ListView负责显示和缓存已经显示过的View;而adapter负责创建View和负责显示界面的内容,如果ListView中已经缓存了,就会从ListView将缓存的View取出传递给adapter,这样adapter就不需要重新创建View了。
这里先看Adapter的这个接口的功能:
public interface Adapter {
// 数据是由adapter来维护的,所有当数据有变化时,只有adapter清楚,
// 这样当数据更改并要刷新界面时,这个功能就得由adapter来完成了,
// 这里注册DataSetObserver的作用就是用来通知系统需要刷新界面了,
// 调用notifyDataSetChanged()请求刷新界面,内部调用的就是observer
void registerDataSetObserver(DataSetObserver observer);
// 上面注册了,这里自然就是解注册了
void unregisterDataSetObserver(DataSetObserver observer);
// 返回数据的条数
int getCount();
// 返回该位置的数据实体
Object getItem(int position);
// 返回该位置的id,一般就直接返回position了
long getItemId(int position);
boolean hasStableIds();
// adapter的主要作用是创建View,然后对生成的View进行填充数据,之后会返回给ListView,
// 直到生成的View能填充一屏数据为止,这个阶段参数convertView是为null的,当滑动屏幕
// 的时候,有些View会滑出屏幕,这时候这个View就需要回收了,也就是缓存起来,同时,
// 也会有view滑进来,这个滑进来的view会先从缓存中去读取,如果有就会以convertView传
// 进来,这个阶段convertView就是缓存起来的view,这样就达到了复用的目的
// convertView需要展示的数据就是通过position来拿到的
View getView(int position, View convertView, ViewGroup parent);
static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;
// 决定了传给getView()中view的类型(如果ListView中有多个不同的item)
// 返回的值在0(包含)到getViewTypeCount()(不包含)之间
// 原因:getViewTypeCount()决定了缓存View的类型,View是通过ArrayList
// 来进行保存的,而ArrayList又是保存在数组中的,而这个数组的大小
// 又是由getViewTypeCount()决定的,而这里返回的值就决定了是从
// 数组的那个位置拿缓存的view
// 这里在举个例子:假如ListView中有四种item,也就是有四种布局,实际也
// 就是四种View了,这种取名为viewTypeA,viewTypeB,viewTypeC,viewTypeD,
// 这里有一点需要注意,这几种view都会保存这里返回的值,决定他们缓存时是
// 缓存在数组的那个位置,这里返回的值一定是0~3,取出时也是由这里决定取哪
// 个位置缓存的view
int getItemViewType(int position);
// 这个方法决定了ListView中可以显示几种View
int getViewTypeCount();
static final int NO_SELECTION = Integer.MIN_VALUE;
// 这里就是判断当前是否有数据,它的作用就是在没有数据时ListView该怎么显示,
// 在ListView中设置setEmptyView(),那么当没有数据时,自然就会显示这个view了
boolean isEmpty();
default @Nullable CharSequence[] getAutofillOptions() {
return null;
}
}
上面已经对adapter中各个接口的作用做了说明,主要作用还是决定了View中显示的内容。
看完adapter后,再来看看ListView中用来缓存View的一个内部类RecycleBin,它位于ListView的父类AbsListView中,它的作用就是用来缓存view以及复用时取出对应类型的view,这里先来看下RecycleBin源码实现:
class RecycleBin {
private RecyclerListener mRecyclerListener;
// 显示在屏幕上第一个View所处的位置
private int mFirstActivePosition;
// 保存的是需要显示在屏幕上view
private View[] mActiveViews = new View[0];
// view移除屏幕后就缓存在这里,
// 这个数组的大小由adapter的getViewTypeCount()决定
private ArrayList<View>[] mScrapViews;
// 缓存View类型的数量
private int mViewTypeCount;
private ArrayList<View> mCurrentScrap;
private ArrayList<View> mSkippedScrap;
private SparseArray<View> mTransientStateViews;
private LongSparseArray<View> mTransientStateViewsById;
// 这个方法在setAdapter()会调用到,viewTypeCount就是adapter的getViewTypeCount()的值
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
//这里创建了一个类型为ArrayList,大小是viewTypeCount的数组,就是用来缓存移出屏幕的View
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;
}
// 清楚所有缓存起来的view
void clear() {
if (mViewTypeCount == 1) {
final ArrayList<View> scrap = mCurrentScrap;
clearScrap(scrap);
} else {
final int typeCount = mViewTypeCount;
for (int i = 0; i < typeCount; i++) {
final ArrayList<View> scrap = mScrapViews[i];
clearScrap(scrap);
}
}
clearTransientStateViews();
}
// 这里就是保存所有将要显示在屏幕上的View,childCount就是一屏有多少条数数据
// 这里有个需要注意的地方,layout会执行两遍,第一遍会将adapter中生成的view添
// 加到ListView中,当执行第二遍的时候,再次执行这个方法时,就会将ListView中
// 的子View添加到mActiveViews中,由于第二遍执行的时候也会添加View到ListView中,
// 所以这里就是为了解决这个问题,第二次layout的时候,先将ListView中的子view添
// 加到mActiveViews,然后再将ListView中的子view移除,等到再次需要添加子view的
// 时候,就直接从mActiveViews拿了
void fillActiveViews(int childCount, int firstActivePosition) {
if (mActiveViews.length < childCount) {
mActiveViews = new View[childCount];
}
mFirstActivePosition = firstActivePosition;
//noinspection MismatchedReadAndWriteOfArray
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;
// Remember the position so that setupChild() doesn't reset state.
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
// 这里就是返回需要显示的view,并移除
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;
}
// 这里就是从缓存中取对应view
View getScrapView(int position) {
// getItemViewType()返回的值就是作为mScrapViews下标的索引
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
// 只有一种类型的view就是从这里返回
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
// 有多种类型的view时就是从这里返回
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
// 这里就是将滑出的view缓存起来
void addScrapView(View scrap, int position) {
final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
lp.scrappedFromPosition = position;
// 这里lp.viewType的值就是getItemViewType()的值,正好和前面取缓存view对应上了
final int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
getSkippedScrap().add(scrap);
}
return;
}
......
final boolean scrapHasTransientState = scrap.hasTransientState();
if (scrapHasTransientState) {
......
} else {
clearScrapForRebind(scrap);
if (mViewTypeCount == 1) {
mCurrentScrap.add(scrap);
} else {
//这里就是缓存view,这个viewType就是getItemViewType()的值
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
}
// 这里就是返回缓存起来的view,也就是复用View
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
final int size = scrapViews.size();
if (size > 0) {
// See if we still have a view for this position or ID.
// Traverse backwards to find the most recently used scrap view
for (int i = size - 1; i >= 0; i--) {
final View view = scrapViews.get(i);
final AbsListView.LayoutParams params =
(AbsListView.LayoutParams) view.getLayoutParams();
if (mAdapterHasStableIds) {
final long id = mAdapter.getItemId(position);
if (id == params.itemId) {
return scrapViews.remove(i);
}
} else if (params.scrappedFromPosition == position) {
//一般都是执行这里
final View scrap = scrapViews.remove(i);
clearScrapForRebind(scrap);
return scrap;
}
}
final View scrap = scrapViews.remove(size - 1);
clearScrapForRebind(scrap);
return scrap;
} else {
return null;
}
}
}
看完上面的代码,这里先来做一些总结:
1、Adapter主要负责数据的处理;
2、Adapter当没有复用View的时候,负责创建View;
3、Adapter在拿到View之后,对View中需要的数据进行填充;
4、RecycleBin主要是对View的缓存和复用的逻辑处理;
上面分析完后,接下来就是该考虑创建出来的VIew是如何显示的,分析的是ListView,那自然是在ListView中去寻找了,这里我们看下ListView的layoutChildren()这个方法,这里需要注意一点,绘制一次,这个方法会执行两次:
protected void layoutChildren() {
......
try {
......
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
// 这里初次进来的时候,返回的是0,不是初次进来的时候返回的就是将显示在界面上View的数量
final int childCount = getChildCount();
......
// 记录第一个显示View的位置,
final int firstPosition = mFirstPosition;
// 这里拿到RecycleBin对象,方便对view的缓存
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
// 数据发生变化时才会执行到这里,这里实际就是将所有的子View缓存起来
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition + i);
}
} else {
// 数据没有变化时会执行到这里,第一次布局的时候childCount = 0,
// 第二次执行的时候会将所有的View添加到RecycleBin的mActiveViews中
recycleBin.fillActiveViews(childCount, firstPosition);
}
// 这里在第二次布局的时候会将第一次添加的view去不清除,这样就不会产生一份重复的数据,
// 前面已经将ListView中的view添加到了mActiveViews中,虽说这里进行了解绑,但再次添加
// 的时候是直接将mActiveViews中的添加进去就可以了,所以对性能没什么影响
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
case LAYOUT_SET_SELECTION:
......
break;
case LAYOUT_SYNC:
sel = fillSpecific(mSyncPosition, mSpecificTop);
break;
case LAYOUT_FORCE_BOTTOM:
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP:
......
break;
case LAYOUT_SPECIFIC:
......
break;
case LAYOUT_MOVE_SELECTION:
sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
break;
default:
// 默认情况下都是普通模式LAYOUT_NORMAL,所有会执行到这里,第一次childCount = 0,
// 第二次执行到这里的时候ListView中有子View,childCount就不等于0
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) {
// 有选中的view时会执行这里
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) {
// 首个显示的View的位置小于adapter中数据的条数
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
......
}
}
从上面看下来,第一次布局的时候会执行fillFromTop(childrenTop),这里就是对ListView中的子view进行布局,跟着进去瞧一瞧,看看他是如何实现的:
/**
* 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);
}
这个方法中没有什么逻辑,就是先判断第一个显示view的位置的合法性,从注释中可以知道,这里的功能是自顶部到底部开始填充,看来具体的实现逻辑要去看看fillDown()这个方法了:
private View fillDown(int pos, int nextTop) {
View selectedView = null;
// 底部距离减去顶部距离就是可以用来填充view的像素值
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
// 如果设置了padding,这里还需要减去padding的距离
end -= mListPadding.bottom;
}
// nextTop是第一个view在屏幕显示的位置,传进来的pos是显示在屏幕上的第一
// 个view在adapter中的位置,没循环一次,这个值会加1
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
// 这里就是创建或获取view
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
// 计算下一个view在屏幕所处的位置,以确定是否填满了屏幕,填满了屏幕就会跳出这个循环
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
pos++;
}
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
在这个循环中,主要的还是获取view,其他的逻辑也是基于这个view的,那这里就去看看makeAndAddView()方法了:
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
if (!mDataChanged) {
// 这里是看是否能拿到一个可以复用的view,前面有提到在第二次布局的时候会将第一次的view
// 清除掉,这里就是再次去拿清除掉的view
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
// 当有复用的view返回时执行到这这里
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// 当没有复用的view时,就是通过这个方法去创建view
final View child = obtainView(position, mIsScrap);
// 在拿到view之后,还没有将view添加到ListView中,那这个方法就是测量view并添加到ListView中去
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
这里获取view有两种方式,一种是拿之前缓存的view,如果之前没有缓存,那么就会重新创建一个view,接下来就去看看obtainView()是如何去创建view的:
View obtainView(int position, boolean[] outMetadata) {
......
// 下面这两个方法可以说是ListView缓存的主要逻辑了,mRecycler.getScrapView(position)是获取缓存的view,
// 而mAdapter.getView(position, scrapView, this)这个方法是不是很熟悉,就是我们重新Adapter中getView()方法,
// 一开始没有缓存时,scrapView返回的是null,所以getView()中传进去的view参数就为null,这是就需要我们创建view了,
// 而当scrapView返回不为null时,这时传进去的view我们就可以直接复用了,这也就是为什么我们一般在getView()中要
// 对传进去的view进行判断,如果为null就创建,不为null就直接使用了,
final View scrapView = mRecycler.getScrapView(position);
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
// 如果传进去的View没被复用,而是重新创建了view,那么会将传进去的view再次添加进复用池中
mRecycler.addScrapView(scrapView, position);
} else if (child.isTemporarilyDetached()) {
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
}
}
if (mCacheColorHint != 0) {
child.setDrawingCacheBackgroundColor(mCacheColorHint);
}
if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
// 这个方法中就是给生成的view设置一些参数,这下参数中就包括对view的分类
setItemViewLayoutParams(child, position);
......
return child;
}
这个方法中的主要逻辑就是生成view还是复用view,当然setItemViewLayoutParams()这个方法也是挺有意思的,它主要是对view的一些参数进行设置,这里去看下它对view的那些参数进行了设置:
private void setItemViewLayoutParams(View child, int position) {
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
LayoutParams lp;
if (vlp == null) {
lp = (LayoutParams) generateDefaultLayoutParams();
} else if (!checkLayoutParams(vlp)) {
lp = (LayoutParams) generateLayoutParams(vlp);
} else {
lp = (LayoutParams) vlp;
}
if (mAdapterHasStableIds) {
lp.itemId = mAdapter.getItemId(position);
}
// 看这里,还记得getItemViewType()这个Adapter中方法么,这个方法返回的就是view属于哪一种类型,
// 当我们在RecycleBin这个类中对view进行缓存的时候用到的就是view的这个viewType
lp.viewType = mAdapter.getItemViewType(position);
lp.isEnabled = mAdapter.isEnabled(position);
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
这个方法中我们主要看对viewType的赋值,这个值对于view的缓存很重要,它区分生成的view时属于哪一类的(item中有多种布局),这也说明了Adapter中getItemViewType()这个方法的作用了。获取view就分析到这了,接下来让我们返回到makeAndAddView()方法,接下来我们再看下它的setupChild()方法做了些什么东西:
/**
* Adds a view as a child and make sure it is measured (if necessary) and
* positioned properly.
* 添加一个view作为子view确保它被测量和放置到合适的位置
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
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 = !isAttachedToWindow || updateChildSelected
|| child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make
// some up...
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
// Set up view state before attaching the view, since we may need to
// rely on the jumpDrawablesToCurrentState() call that occurs as part
// of view attachment.
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
if (child instanceof Checkable) {
((Checkable) child).setChecked(mCheckStates.get(position));
} else if (getContext().getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.HONEYCOMB) {
child.setActivated(mCheckStates.get(position));
}
}
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
// 第二次布局时会执行到这里,前面有提到会将第一次添加的view detach掉,
// 这时只需要将detach的view再次attachViewToParent()就可以了
attachViewToParent(child, flowDown ? -1 : 0, p);
// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position) {
child.jumpDrawablesToCurrentState();
}
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
// 将view添加进父布局中,这里其实就是添加到ListView中,第一次布局会执行到这里
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded();
}
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);
}
// 对子view进行测量
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;
// 对子view进行布局
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
这里可以分为三个步骤,一是将已经生成的view添加进ListView;二是对添加进去的view进行测量;三是对添加进去的view进行布局,这样一次完整的流程就完成了。