首先扯点别的:准备把ListView和RecyclerView的源码都看一遍,然后记录一下。网上已经有很多相关文章写的很好了,准备对照源码,参考别人的文章,然后整理出自己的文章来,形成自己的体系。
源码版本:28
本篇要点
- ListView的缓存类AbsListView.RecycleBin。
- 设置适配器后的流程。
- ListView的measure、layout、draw流程,其中包含自定义的适配器的getView方法的调用。
- 适配器调用notifyDataSetChanged方法后的流程。
ListView的缓存类AbsListView.RecycleBin
public abstract class AbsListView extends AdapterView<ListAdapter> {
/**
* 用来存储未被使用到的views,这些views在下一次layout过程中应该会被用到从而避免创建新的views。
*/
final RecycleBin mRecycler = new RecycleBin();
/**
* RecycleBin用来在布局过程中重用views。RecycleBin有两级存储:ActiveViews 和 ScrapViews。
* ActiveViews是那些在布局开始的时候在屏幕上的View。在布局结束的时候,ActiveViews中未被用到的View会被移动到ScrapViews中。
* ScrapViews是老的View,这些View可能被适配器使用,从而避免创建新的View。
*/
class RecycleBin {
/**
* 在布局(layout)开始的时候在屏幕上的Views。在布局开始的时候会将屏幕上的Views添加到mActiveViews中。在布局结束的时候,
* mActiveViews中的Views会被移动到mScrapViews中。mActiveViews中的Views一个范围内连续的Views。
* 第一个View的存储在mActiveViews中下标为mFirstActivePosition的位置上。
*/
private View[] mActiveViews = new View[0];
/**
* 未排序的Views可以作为 ConvertView被适配器使用。
*/
private ArrayList<View>[] mScrapViews;
/**
* 当只有一种ViewType类型的时候,mCurrentScrap是mScrapViews的第一个元素。
*/
private ArrayList<View> mCurrentScrap;
//...
}
}
RecycleBin有两级存储:mActiveViews和mScrapViews。mActiveViews存储的是那些在布局(layout)开始的时候在屏幕上的View,在布局结束的时候,mActiveViews中未被用到的View会被移动到mScrapViews中。mScrapViews中还存储着滑出屏幕的的View,这些View可能被适配器使用,从而避免创建新的View。
mScrapViews是一个元素为ArrayList<View>
的数组,对应适配器多种ViewType,当只有一种ViewType类型的时候,mCurrentScrap是mScrapViews的第一个元素。
ListView基本的使用方法
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
自定义适配器
class ListViewAdapter(
context: Context,
private val resource: Int,
lists: ArrayList<MyBean>
) : ArrayAdapter<MyBean>(context, resource, lists) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
val holder: ViewHolder
val bean = getItem(position)
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resource, parent, false)
holder = ViewHolder()
holder.textViewTitle = view.findViewById(R.id.tvTitle)
holder.textViewDetail = view.findViewById(R.id.tvDetail)
view.tag = holder
} else {
//注释1处,返回的convertView不为null,直接复用
view = convertView
holder = view.tag as ViewHolder
}
//绑定数据
holder.textViewTitle?.text = bean?.title
holder.textViewDetail?.text = bean?.detail
return view
}
//Holder类,用来提高listView的性能
private class ViewHolder {
var textViewTitle: TextView? = null
var textViewDetail: TextView? = null
}
}
注释1处,返回的convertView不为null,复用。
ListView设置适配器
adapter = ListViewAdapter(this, R.layout.item_list_view, list)
listView.adapter = adapter
基本使用应该都很熟了,不再赘述。
ListView的setAdapter方法
@Override
public void setAdapter(ListAdapter adapter) {
//移除老的观察者
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
//注释1处,重置某些变量
resetList();
//注释2处,清空缓存中所有等待复用的View
mRecycler.clear();
//如果ListView添加了header或者footer,则包装适配器
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
mAdapter = adapter;
}
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
// AbsListView#setAdapter will update choice mode states.
super.setAdapter(adapter);
if (mAdapter != null) {
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
//mItemCount,适配器中数据的数量,通常就是我们传入适配器中list的size
mItemCount = mAdapter.getCount();
checkFocus();
//注册新的观察者,观察适配器的数据变化,数据变化后ListView会调用requestLayout方法
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
//mRecycler设置适配器ViewType的数量。
mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
//...
}
//注释3处,最重要的一步,请求measure、layout、draw
requestLayout();
}
ListView的resetList方法
@Override
void resetList() {
//...
//调用AbsListView的resetList方法
super.resetList();
//将布局模式设置为LAYOUT_NORMAL
mLayoutMode = LAYOUT_NORMAL;
}
AbsListView的resetList方法
void resetList() {
//移除所有的子View
removeAllViewsInLayout();
//将mFirstPosition置为0
mFirstPosition = 0;
//mDataChanged置为false
mDataChanged = false;
mPositionScrollAfterLayout = null;
mNeedSync = false;
mPendingSync = null;
mOldSelectedPosition = INVALID_POSITION;
mOldSelectedRowId = INVALID_ROW_ID;
setSelectedPositionInt(INVALID_POSITION);
setNextSelectedPositionInt(INVALID_POSITION);
mSelectedTop = 0;
mSelectorPosition = INVALID_POSITION;
mSelectorRect.setEmpty();
invalidate();
}
resetList方法内部会将mLayoutMode置为LAYOUT_NORMAL,移除所有的子View,将mDataChanged置为false。适配器调用notifyDataSetChanged以后,mDataChanged为true。这里先提一下。
setAdapter方法注释3处,最重要的一步,调用requestLayout方法请求measure、layout、draw。
ListView 的测量过程
ListView的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
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;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (heightMode == MeasureSpec.AT_MOST) {
//注释1处,决定最终高度,最后一个参数是-1注意一下。
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
//保存宽高信息
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
我们平时使用ListView的宽高一般都是 MATCH_PARENT,有时候高度会设置为WRAP_CONTENT,这个时候测量模式就是 MeasureSpec.AT_MOST 。注释1处,如果高度的是测量模式是 MeasureSpec.AT_MOST,则会调用measureHeightOfChildren方法。
ListView 的 measureHeightOfChildren 方法。
/**
* 测量指定范围内的子View的高度,返回的高度包括ListView的padding和分割线的高度。
* 如果指定了最大高度,那么当测量高度到达最大高度以后测量会停止测量。
*
* @param widthMeasureSpec The width measure spec to be given to a child's
* {@link View#measure(int, int)}.
* @param startPosition The position of the first child to be shown.
* @param endPosition The (inclusive) position of the last child to be
* shown. Specify {@link #NO_POSITION} if the last child should be
* the last available child from the adapter.
* @param maxHeight 如果指定范围内子View的累加高度超过了maxHeight,就返回maxHeight。
* @param disallowPartialChildPosition 通常情况下,返回的高度是否只包括完整的View ,暂时不关注。
* @return The height of this ListView with the given children.
*/
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
// 指定范围内子View的所有累加高度,包括ListView的 padding 和 分割线高度。
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = mDividerHeight;
// The previous height value that was less than maxHeight and contained
// no partial children
int prevHeightWithoutPartialChild = 0;
int i;
View child;
// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
//注释1处,在MeasureSpec.AT_MOST模式下是否在测量过程中将View加入到recycleBin,默认是true
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
for (i = startPosition; i <= endPosition; ++i) {
//获取子View
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
// 是否在测量过程中将 View 加入到 recycleBin
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}
returnedHeight += child.getMeasuredHeight();
if (returnedHeight >= maxHeight) {
//...
return maxHeight;
}
//...
}
// 指定范围内所有的子View累加高度没有超过 maxHeight,就返回returnedHeight。
return returnedHeight;
}
注释1处,在MeasureSpec.AT_MOST模式下是否在测量过程中将View加入缓存,默认是true。那么就是说在MeasureSpec.AT_MOST模式下,第一次在测量过程中,就会通过obtainView获取View(第一次测量获取的View是新创建的)并加入到缓存中。
这里我们先不关注这种场景,我们以ListView的宽高都是都是 MATCH_PARENT 的情况,即宽高的测量模式都是 MeasureSpec.EXACTLY 模式来分析。所以我们简单的认为ListView 的 onMeasure 方法就是简单的保存了宽高信息即可。
ListView 的布局过程
ListView 的 onLayout 方法
注意:开始第一次布局的时候,此时所有的数据都在适配器中,而ListView是没有子View的(即ListView还没有调用过addViewInLayout方法添加子View),所以 getChildCount = 0 。
AbsListView 的 onLayout 方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//标记正在layout,避免重复layout
mInLayout = true;
//注释1处,此时获取的 childCount 为 0 。
final int childCount = getChildCount();
if (changed) {
//注释2处,ListView位置或者大小发生了改变,强制子View在下一个layout过程中重新布局
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
//注释3处,布局子View
layoutChildren();
//...
//布局结束后,将mInLayout置为false
mInLayout = false;
}
注释1处,此时获取的childCount 为0 。
注释2处,ListView位置或者大小发生了改变,changed为true。强制子View在下一个layout过程中重新布局。比如我们在第一次布局测量出来ListView的宽高是1080 * 1960,这时候changed就是true。
注释3处,布局子View。
ListView的layoutChildren方法。
@Override
protected void layoutChildren() {
//如果阻止布局就直接返回。
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
//布局子View最上面的坐标
final int childrenTop = mListPadding.top;
//布局子View最下面的坐标
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
//注释1处,记录ListView当前的子View个数,第一次布局的时候为0。
final int childCount = getChildCount();
int index = 0;
int delta = 0;
//...
//只有在调用adapter.notifyDataSetChanged()方法一直到layout()布局结束,dataChanged为true,默认为false
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
//mItemCount是mAdapter.getCount()返回的数据的数量
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
//在布局过程中,adapter的数据发生了改变但是没调用notifyDataSetChanged方法会抛出异常。
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. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
//...
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {//数据发生了改变,将所有的子View加入到RecycleBin。这些View将来可能会被复用。
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
//注释2处,dataChanged为false,会走到这里
recycleBin.fillActiveViews(childCount, firstPosition);
}
//注释3处,将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
//测量模式默认是LAYOUT_NORMAL,会走到 default分支
switch (mLayoutMode) {
//...
default:
//注释4处,此时childCount为0
if (childCount == 0) {
//mStackFromBottom标记是否从ListView的底部向上填充,默认是false
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
//注释5处
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;
}
//注释6处,将mActiveViews没有用到的View移动到mScrapViews中。
recycleBin.scrapActiveViews();
//...
//将mLayoutMode置为LAYOUT_NORMAL
mLayoutMode = LAYOUT_NORMAL;
//将mDataChanged置为false
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
//...
}
}
注释1处,记录ListView当前的子View个数,第一次布局的时候为0。
注释2处,dataChanged为false,RecycleBin调用fillActiveViews方法。
RecycleBin的fillActiveViews方法。
/**
* 使用AbsListView的所有子View填充 ActiveViews 。
*
* @param childCount mActiveViews要持有的View的最少数量。
* @param firstActivePosition ListView中第一个要存储在mActiveViews的子View在ListView中的位置。
*/
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();
//不要将headView和footView加入mActiveViews
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
如果ListView已经有子View了,即childCount大于0,就将子View添加到mActiveViews数组中,第一次布局的时候childCount为0,该方法调用无意义。
注释3处,调用detachAllViewsFromParent方法将子View和ListView取消关联。此时ListView中是没有子View的,所以调用此方法也没意义。注意:ViewGroup的子类调用此方法以后getChildCount返回值为0。
注释4处,此时childCount为0。
mStackFromBottom标记是否从ListView的底部向上填充,默认是false,所以会走到注释5处。调用fillFromTop方法。
ListView的fillFromTop方法。
/**
* 从mFirstPosition开始,从上到下填充ListView。
*
* @param nextTop 第一个要被绘制的itemView的top坐标。
*
* @return 当前选中的itemView
*/
private View fillFromTop(int nextTop) {
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
}
//调用fillDown方法
return fillDown(mFirstPosition, nextTop);
}
ListView的fillDown方法
/**
* 从pos开始向下填充ListView。
*
* @param pos 第一个要填充到ListView的位置。
*
* @param nextTop 第一个要填充到ListView的位置的itemView的top坐标。
*
* @return 如果当前选中的itemView出现在我们的绘制范围内的话,则返回该itemView。否则返回null。
*/
private View fillDown(int pos, int nextTop) {
View selectedView = null;
//ListView的底部坐标
int end = (mBottom - mTop);
//注释1处,如果要填充的itemView的top坐标小于end并且pos小于mItemCount,则循环填充。
while (nextTop < end && pos < mItemCount) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
//注释2处,创建并将子View添加到ListView中
View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
//注释3处,累加nextTop,包含了分割线的高度mDividerHeight
nextTop = child.getBottom() + mDividerHeight;
if (selected) {
selectedView = child;
}
//累加pos
pos++;
}
return selectedView;
}
注释1处,如果要填充的itemView的top坐标小于end并且pos小于mItemCount,则循环填充。mItemCount是适配器中数据的数量。
我们先看下注释3处,累加nextTop,如果要添加分割线的话下一个子View的top坐标是加上了分割线的高度mDividerHeight。这样就是说每个子View之前空出来了一个分割线的高度,然后我们在绘制的时候,在每个子View空出来的位置绘制分割线,然后再绘制子View。
注意:ListView并不是绘制一个子View绘制一个分割线这样交替绘制的。而是先把所有要绘制的分割线绘制出来,然后在绘制子View。
我们回过头来在看注释2处,创建并将子View添加到ListView中。
ListView 的 makeAndAddView 方法。
/**
* 获取View并将其添加到子View列表中。View可以是新创建的,从未被使用的View转化来的,或者从RecyclerBin中获取的。
*
* @param position 该View在ListView的mChildren数组中的位置。
* @param y 该View被添加到ListView的top坐标或者bottom坐标。
* @param flow true 将View的上边界和y对齐。即 y是View的top坐标。 false 将View的下边界和y对齐。即 y是View的bottom坐标。
* @param childrenLeft View的left坐标。
* @param selected 这个位置上的View是否被选中。
* @return 该View
*/
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
// 默认情况mDataChanged==false,在调用adapter.notifyDataSetChanged()之后的
// layout阶段中mDataChanged == true
if (!mDataChanged) {
//注释1处,第一次布局的时候childCount为0,getActiveView是获取不到的。
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
//找到可直接使用的View,将其添加到ListView中去。
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
//注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。
final View child = obtainView(position, mIsScrap);
//注释3处 通过obtainView获取到的View需要被重新测量和布局。注意调用setupChild方法的最后一个参数。
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
注释1处,首先从mRecycler的mActiveViews数组中尝试获取可复用的View,在前面layoutChildren方法中,如果mDataChanged为false会尝试把View放到mRecycler的mActiveViews中保存。第一次布局的时候childCount为0,mRecycler的mActiveViews数组是空数组,getActiveView是获取不到的。
注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。
AbsListView的obtainView方法。
/**
* 获取一个View并让它展示指定位置上的数据。
*
* @param position the position to display
* @param outMetadata
*
* @return 返回一个展示了指定位置上的数据的View
*/
View obtainView(int position, boolean[] outMetadata) {
//...
//注释1处,先从mRecycler的mScrapViews数组中获取一个在滑动时候废弃保存的子view
final View scrapView = mRecycler.getScrapView(position);
//注释2处,调用平时写的adapter的getView()方法获取View,注意第二个参数传入了scrapView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
//注释3处,如果child != scrapView和不相等,就是说我们无法复用scrapView,
//那么就再将scrapView保存起来后续复用。
mRecycler.addScrapView(scrapView, position);
}
//...
}
//...
//返回获取到的View
return child;
}
注释1处,先从mRecycler的mScrapViews数组中获取一个在滑动时候废弃保存的子View。第一个布局的时候是获取不到的。
注释2处,调用适配器的getView方法获取View,注意第二个参数传入了scrapView。
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View
val holder: ViewHolder
val bean = getItem(position)
//开始convertView为null,创建新的View返回
if (convertView == null) {
//注意这里会为创建的View设置ListView的布局参数AbsListView.LayoutParams
view = LayoutInflater.from(context).inflate(resource, parent, false)
holder = ViewHolder()
holder.textViewTitle = view.findViewById(R.id.tvTitle)
holder.textViewDetail = view.findViewById(R.id.tvDetail)
view.tag = holder
} else {
//convertView不为null的时候,直接使用convertView
view = convertView
holder = view.tag as ViewHolder
}
//将View与数据绑定以后返回
holder.textViewTitle?.text = bean?.title
holder.textViewDetail?.text = bean?.detail
return view
}
在适配器的getView方法中,如果convertView == null,那么我们需要新创建View,否则的话直接复用convertView,将convertView和新的数据绑定就行。
我们回到makeAndAddView方法的注释3处ListView的setupChild方法。
/**
* 将一个子View添加到ListView中去,如果必要的话测量子View并将子View布局在合适的位置。
*
* @param child the view to add
* @param position the position of this child
* @param y 要添加的View的top坐标或者bottom坐标
* @param flowDown true的话,y参数是要添加的View的top坐标,false,y参数是View的bottom坐标。
* @param childrenLeft left edge where children should be positioned
* @param selected {@code true} if the position is selected, {@code false}
* otherwise
* @param isAttachedToWindow 要添加的View是否已经添加到window上了。
*/
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();
//注释1处,如果child未被添加到ListView中过,或者选中状态发生了改变,或者调用了child的forceLayout方法,则需要重新测量child。
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();
}
//child的类型viewType
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
//...
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
//注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。
attachViewToParent(child, flowDown ? -1 : 0, p);
//...
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
//注释3处,将child添加到ListView。
addViewInLayout(child, flowDown ? -1 : 0, p, true);
//...
}
//需要测量child
if (needToMeasure) {
//...
//注释4处,测量Child
child.measure(childWidthSpec, childHeightSpec);
} else {
//清除child的FLAG_FORCE_LAYOUT标记,避免多次测量和布局View
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;
//注释5处,布局child
child.layout(childrenLeft, childTop, childRight, childBottom);
} else {
//修正child的坐标
child.offsetLeftAndRight(childrenLeft - child.getLeft());
child.offsetTopAndBottom(childTop - child.getTop());
}
//...
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
注释1处,如果child未被添加到ListView中过,或者选中状态发生了改变,或者调用了child的forceLayout方法,则需要重新测量child。
注意当ListView的宽高测量模式的都是EXACTLY的情况下,在自己的测量过程中是没有测量child的,所以在第一次调动setupChild会测量子View。
注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。
ViewGroup的attachViewToParent方法。
protected void attachViewToParent(View child, int index, LayoutParams params) {
child.mLayoutParams = params;
if (index < 0) {
index = mChildrenCount;
}
//将child加入到ListView的mChildren中
addInArray(child, index);
//将child的父View设置为ListView
child.mParent = this;
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
& ~PFLAG_DRAWING_CACHE_VALID)
| PFLAG_DRAWN | PFLAG_INVALIDATED;
this.mPrivateFlags |= PFLAG_INVALIDATED;
}
注释3处,需要调用addViewInLayout将child添加到ListView。
ViewGroup的addViewInLayout方法。再往里面的逻辑我们就不跟了。大致逻辑就是将child添加到ViewGroup的mChildren中,并将child attach 到当前窗口。只有调用完addViewInLayout方法以后,ListView的getChildCount才大于0。
protected boolean addViewInLayout(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
child.mParent = null;
//调用addViewInner添加。
addViewInner(child, index, params, preventRequestLayout);
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
return true;
}
setupChild方法,注释4处,测量Child。
注释5处,布局child。
到这里ListView的布局过程就结束了。
注意:第一次布局结束子View已经测量完毕并布局到正确的位置了,接下来就是绘制了。
ListView绘制子View
我们先看ListView的dispatchDraw方法。
@Override
protected void dispatchDraw(Canvas canvas) {
final int dividerHeight = mDividerHeight;
final Drawable overscrollHeader = mOverScrollHeader;
final Drawable overscrollFooter = mOverScrollFooter;
final boolean drawOverscrollHeader = overscrollHeader != null;
final boolean drawOverscrollFooter = overscrollFooter != null;
final boolean drawDividers = dividerHeight > 0 && mDivider != null;
//需要绘制分割线,则先绘制绘制分割线
if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
// Only modify the top and bottom in the loop, we set the left and right here
final Rect bounds = mTempRect;
bounds.left = mPaddingLeft;
bounds.right = mRight - mLeft - mPaddingRight;
final int count = getChildCount();
//...
if (!mStackFromBottom) {
int bottom = 0;
//...
for (int i = 0; i < count; i++) {
final int itemIndex = (first + i);
final boolean isHeader = (itemIndex < headerCount);
final boolean isFooter = (itemIndex >= footerLimit);
if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
final View child = getChildAt(i);
bottom = child.getBottom();
final boolean isLastItem = (i == (count - 1));
if (drawDividers && (bottom < listBottom)
&& !(drawOverscrollFooter && isLastItem)) {
final int nextIndex = (itemIndex + 1);
// Draw dividers between enabled items, headers
// and/or footers when enabled and requested, and
// after the last enabled item.
if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
&& (nextIndex >= headerCount)) && (isLastItem
|| adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
&& (nextIndex < footerLimit)))) {
bounds.top = bottom;
bounds.bottom = bottom + dividerHeight;
//绘制分割线
drawDivider(canvas, bounds, i);
}
//...
}
}
}
//...
}
//...
}
// 绘制子View
super.dispatchDraw(canvas);
}
ListView重写了dispatchDraw方法,需要绘制分割线的话,ListView会先绘制分割线,再绘制子View。
注意:ListView并不是绘制一个子View绘制一个分割线这样交替绘制的。而是先把所有要绘制的分割线绘制出来,然后在绘制子View。
当绘制结束以后,ListView就可以正常显示了。
注意:在测试过程中发现,ListView会经过至少两次测量和布局过程。不知道第二次布局是怎么触发的。
class MyListView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ListView(context, attrs, defStyleAttr) {
private val TAG: String = "MyListView"
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
Log.i(TAG, "onMeasure: measuredWidth = $measuredWidth , measuredHeight =$measuredHeight")
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
Log.i(TAG, "onLayout: ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight changed = $changed")
}
}
打印日志
ListView两次布局.png但是我发现只布局一次就能正确显示啊。
package com.hm.viewdemo.widget
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.widget.ListView
import java.util.logging.Logger
/**
* Created by dumingwei on 2021/1/24.
*
* Desc:
*/
class MyListView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ListView(context, attrs, defStyleAttr) {
private var layoutCount = 0
private var mMotionY = 0
private val TAG: String = "MyListView"
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
Log.i(TAG, "onMeasure: childCount ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight")
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//注释1处
if (layoutCount == 1) {
return
}
layoutCount++
super.onLayout(changed, l, t, r, b)
Log.i(TAG, "onLayout: childCount ${childCount} measuredWidth = $measuredWidth , measuredHeight =$measuredHeight changed = $changed")
}
}
注释1处,如果布局完一次以后,就直接返回不进行第二次布局。
打印日志
ListView一次布局.png这里我们就不再纠结一次布局两次布局的问题了,我们也来分析一下在第二次布局过程中layoutChildren方法的逻辑。
@Override
protected void layoutChildren() {
//如果阻止布局就直接返回。
final boolean blockLayoutRequests = mBlockLayoutRequests;
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren();
invalidate();
if (mAdapter == null) {
resetList();
invokeOnItemScrollListener();
return;
}
//布局子View最上面的坐标
final int childrenTop = mListPadding.top;
//布局子View最下面的坐标
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
//注释1处,记录ListView当前的子View个数,第二次布局的时候大于0。
final int childCount = getChildCount();
int index = 0;
int delta = 0;
//...
//只有在调用adapter.notifyDataSetChanged()方法一直到layout()布局结束,dataChanged为true,默认为false
boolean dataChanged = mDataChanged;
if (dataChanged) {
handleDataChanged();
}
//mItemCount是mAdapter.getCount()返回的数据的数量
if (mItemCount == 0) {
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) {
//在布局过程中,adapter的数据发生了改变但是没调用notifyDataSetChanged方法会抛出异常。
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. Make sure your adapter calls notifyDataSetChanged() "
+ "when its content changes. [in ListView(" + getId() + ", " + getClass()
+ ") with Adapter(" + mAdapter.getClass() + ")]");
}
//...
// 将所有的子View加入到RecycleBin。这些View将来可能会被复用。
// These views will be reused if possible
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {//数据发生了改变,将所有的子View加入到RecycleBin。
for (int i = 0; i < childCount; i++) {
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
//注释2处,dataChanged为false,会走到这里
recycleBin.fillActiveViews(childCount, firstPosition);
}
//注释3处,将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。
detachAllViewsFromParent();
recycleBin.removeSkippedScrap();
//测量模式默认是LAYOUT_NORMAL,会走到 default分支
switch (mLayoutMode) {
//...
default:
//注释4处,此时childCount为0条件不满足。
if (childCount == 0) {
//mStackFromBottom标记是否从ListView的底部向上填充,默认是false
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
//注释5处
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) {
//注释6处
sel = fillSpecific(mFirstPosition,
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
//注释7处,recycleBin将=mActiveViews没有用到的View移动到mScrapViews中。
recycleBin.scrapActiveViews();
//...
//将mLayoutMode置为LAYOUT_NORMAL
mLayoutMode = LAYOUT_NORMAL;
//将mDataChanged置为false
mDataChanged = false;
if (mPositionScrollAfterLayout != null) {
post(mPositionScrollAfterLayout);
mPositionScrollAfterLayout = null;
}
//...
}
}
注释1处,记录ListView当前的子View个数,第二次布局的时候大于0。
注释2处,dataChanged为false,调用RecycleBin的fillActiveViews方法。此时childCount大于0,会将ListView的所有子View加入到mActiveViews中。
/**
* 使用AbsListView的所有子View填充 ActiveViews 。
*
* @param childCount mActiveViews要持有的View的最少数量。
* @param firstActivePosition ListView中第一个要存储在mActiveViews的子View在ListView中的位置。
*/
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();
//不要将headView和footView加入mActiveViews
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
activeViews[i] = child;
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
注释3处,detachAllViewsFromParent(),将子View和ListView取消关联。调用此方法以后getChildCount返回值为0。也就是说ListView不存在子View了。第一次布局后ListView中的View又都从ListView中移除了放进缓存的mActiveViews中。
然后注释6处条件会满足。
/**
* 将一个指定的itemView放置在屏幕上的指定位置上,然后从这个位置开始向上和向下填充ListView。
*
* @param position 指定的itemView要摆放的位置。
* @param top 指定的itemView的top坐标。
*
* @return 选中的View,如果选中的View在可见区域之外,返回null。
*/
private View fillSpecific(int position, int top) {
boolean tempIsSelected = position == mSelectedPosition;
//先填充该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;
if (!mStackFromBottom) {//默认情况下不会从ListView的底部开始填充,mStackFromBottom为false
//向上填充
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) {
//纠正LsitView的高度
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;
}
}
不过其实它和fillUp()、fillDown()方法功能也是差不多的,主要的区别在于,fillSpecific()方法会优先将指定位置的子View先加载到屏幕上,然后再加载该子View往上以及往下的其它子View。
ListView的fillUp方法。
/**
* 从pos开始向上填充ListView。
*
* @param pos 第一个要填充到ListView的位置。
*
* @param nextBottom 要填充到ListView的位置的View的bottom坐标。
*
* @return 当前选中的的View
*/
private View fillUp(int pos, int nextBottom) {
View selectedView = null;
//这时候end为0
int end = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end = mListPadding.top;
}
//nextBottom大于0并且pos大于0,循环填充
while (nextBottom > end && pos >= 0) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
//减小nextBottom,包含分割线的高度mDividerHeight
nextBottom = child.getTop() - mDividerHeight;
if (selected) {
selectedView = child;
}
//减小pos,最终pos会减到-1
pos--;
}
//最终pos会减到-1,所以mFirstPosition要加1变成0
mFirstPosition = pos + 1;
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
return selectedView;
}
ListView的fillDown方法上面看过了,这里就不再看了。我们再看一下makeAndAddView方法的逻辑。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) {
// 默认情况mDataChanged==false,在调用adapter.notifyDataSetChanged()之后的
// layout阶段中mDataChanged == true
if (!mDataChanged) {
//注释1处,这个时候getActiveView是可以获取到的。
final View activeView = mRecycler.getActiveView(position);
if (activeView != null) {
//找到可直接使用的View,将其添加到ListView中去。
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
//注释2处,为该位置创建一个新的View,或者从一个未被使用的View转换。
final View child = obtainView(position, mIsScrap);
//注释3处 通过obtainView获取到的View需要被重新测量和布局。注意调用setupChild方法的最后一个参数。
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
注释1处,这个时候getActiveView是可以获取到的,因为前面在第二次布局layoutChildren方法中,我们调用了RecycleBin的fillActiveViews方法将ListView的子View加入到了RecycleBin的的mActiveViews中。
RecycleBin的getActiveView方法。
/**
* @param position 在ListView当中的位置
*/
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
//注释1处,获取到以后,从mActiveViews移除
activeViews[index] = null;
return match;
}
return null;
}
注释1处,获取到以后,将View从从mActiveViews移除。
然后注释3处,会调用setupChild方法,并且最后一个参数传入的是true。表示该View是添加到当前的窗口上过了的,可以复用,直接再attach的当前窗口即可。
/**
* 将一个子View添加到ListView中去,如果必要的话测量子View并将子View布局在合适的位置。
*
* @param child the view to add
* @param position the position of this child
* @param y 要添加的View的top坐标或者bottom坐标
* @param flowDown true的话,y参数是要添加的View的top坐标,false,y参数是View的bottom坐标。
* @param childrenLeft left edge where children should be positioned
* @param selected {@code true} if the position is selected, {@code false}
* otherwise
* @param isAttachedToWindow 要添加的View是否已经添加到window上了。
*/
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
//...
//注释1处,条件满足
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
//注释2处,表明child是曾经使用过的,直接将child关联到ListView即可。
attachViewToParent(child, flowDown ? -1 : 0, p);
//...
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
//注释3处,将child添加到ListView。
addViewInLayout(child, flowDown ? -1 : 0, p, true);
//...
}
//...
}
注释1处的条件满足。所以会执行注释2处的attachViewToParent()方法,直接将child关联到ListView即可。
注意:第一次Layout过程则是执行的是注释3处的addViewInLayout()方法。这两个方法最大的区别在于,如果我们需要向ViewGroup中添加一个新的子View,应该调用addViewInLayout方法。而如果是想要将一个之前detach的View重新attach到ViewGroup上,就应该调用attachViewToParent方法。那么由于前面在layoutChildren方法当中调用了detachAllViewsFromParent方法,这样ListView中所有的子View都是处于detach状态的,所以这里直接调用attachViewToParent方法,直接将child关联到ListView即可。
适配器的notifyDataSetChanged方法
我们看一下适配器ArrayAdapter的notifyDataSetChanged方法。
我们的适配器间接继承自BaseAdapter。
public void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
我们在ListView设置适配器的时候给适配器注册了一个AdapterDataSetObserver。最终会调用到AdapterDataSetObserver的onChanged方法。
@Override
public void setAdapter(ListAdapter adapter) {
mDataSetObserver = new AdapterDataSetObserver();
mAdapter.registerDataSetObserver(mDataSetObserver);
}
最终要看AdapterView.AdapterDataSetObserver的onChanged方法。
class AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
//将mDataChanged置为true。
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
//最终导致ListView请求重新measure、layout、draw
requestLayout();
}
//...
}
适配器调用notifyDataSetChanged以后,最终导致ListView请求重新measure、layout、draw。
参考链接:
网友评论