背景
在安卓开发学习之ListView的测量流程源码阅读一文中,我记录了ListView的onMeasure()过程,今天,我继续记录下ListView的onLayout()流程
AbsListView#onLayout
ListView并没有实现onLayout(),所以它调用的是父类AbsListView的onLayout()方法,代码如下
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b); // 更新mLayoutHeight
mInLayout = true;
final int childCount = getChildCount(); // childCount是ListView内的子view数量,第一次加载前,值为0
if (changed) { // 布局发生变化的话,将listView里每个子view设置为force_layout
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty(); // 回收站里的子view也不例外
}
layoutChildren(); // 对子view进行布局
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) { // 处理回调
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
可以看到,主要是调用了layoutChildren()方法
ListView#layoutChildren
layoutChildren()方法在ListView中有实现,代码如下
@Override
protected void layoutChildren() {
final boolean blockLayoutRequests = mBlockLayoutRequests; // 同步layout请求
if (blockLayoutRequests) {
return;
}
mBlockLayoutRequests = true;
try {
super.layoutChildren(); // 空方法
invalidate(); // 清空listView
if (mAdapter == null) { // 不考虑adapter为空的时候
resetList();
invokeOnItemScrollListener();
return;
}
final int childrenTop = mListPadding.top; // listView里子view的顶端极限
final int childrenBottom = mBottom - mTop - mListPadding.bottom; // lisrView里子view的底端极限
final int childCount = getChildCount(); // listView中所有子view数目,第一次布局前,还没有addView,childCount = 0
int index = 0;
int delta = 0;
View sel;
View oldSel = null;
View oldFirst = null;
View newSel = null;
// Remember stuff we will need down below
switch (mLayoutMode) { // 根据layout模式执行不同逻辑,一般是default
.. // 其他情况
default:
// Remember the previously selected view
index = mSelectedPosition - mFirstPosition; // 上一次选中的子view位置 = 上一次选中的子view位置 - listView中第一个完整显示的子view位置
if (index >= 0 && index < childCount) {
oldSel = getChildAt(index); // 上一次选中的子view
}
// Remember the previous first child
oldFirst = getChildAt(0); // 上一次listView里第一个完整显示的子view
if (mNextSelectedPosition >= 0) {
delta = mNextSelectedPosition - mSelectedPosition; // 这一次和上一次选中的子view位置之差
}
// Caution: newSel might be null
newSel = getChildAt(index + delta); // 这一次选中的子view
}
// 第一次布局时,由于childCount = 0,listView里没有子view,所以oldSel和newSel都是null
boolean dataChanged = mDataChanged; // 第一次布局时,dataChanged为false
if (dataChanged) {
handleDataChanged(); // adapter里的数据发生变化后,调用这个方法,跟布局有关的,是改变了layout_mode,正序显示的话就是force_top,逆序显示则为force_bottom
}
// Handle the empty set by removing all views that are visible
// and calling it a day
if (mItemCount == 0) { // mItemCount是adapter.getItemCount()返回的,不应该是0
resetList();
invokeOnItemScrollListener();
return;
} else if (mItemCount != mAdapter.getCount()) { // 这种情况也不应该出现
.. // 抛异常
}
setSelectedPositionInt(mNextSelectedPosition);
/* 这是ListView爷爷类AdapterView的方法,代码如下
void setSelectedPositionInt(int position) {
mSelectedPosition = position; // 更新mSelectedPosition
mSelectedRowId = getItemIdAtPosition(position); // 判空后,调用adapter.getItemId()方法
}
*/
.. // 无障碍处理
.. // 获取焦点并处理
// Pull all children into the RecycleBin.
// These views will be reused if possible
final int firstPosition = mFirstPosition; // listView中第一个完整显示的view
final RecycleBin recycleBin = mRecycler;
if (dataChanged) { // 数据发生改变
for (int i = 0; i < childCount; i++) { // 把listView中的子view缓存到回收站中
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else { // 数据没改变,第一次布局的话,childCount为0,这个方法成了摆设,之后的布局它才有用,就是把listView里的子view缓存到了mActiveViews里
recycleBin.fillActiveViews(childCount, firstPosition);
}
// Clear out old views
detachAllViewsFromParent(); // 清空所有子view,全部分离掉
recycleBin.removeSkippedScrap(); // itemTypeCount为1的话,这个方法就是摆设
switch (mLayoutMode) {
.. // 其他情况
case LAYOUT_FORCE_BOTTOM: // 数据源发生变化,逆序显示时
sel = fillUp(mItemCount - 1, childrenBottom);
adjustViewsUpOrDown();
break;
case LAYOUT_FORCE_TOP: // 数据源发生变化,正序显示时
mFirstPosition = 0;
sel = fillFromTop(childrenTop);
adjustViewsUpOrDown(); // 调整子view的位置
break;
.. // 其他情况
default: // 数据源没有变化,正常是这种情况
if (childCount == 0) { // 第一次加载的话,childCount = 0
if (!mStackFromBottom) { // 是否按着adapter的数据顺序显示,mStackFromBottom为false表示顺序显示,为true表示逆序显示,一般就是顺序
final int position = lookForSelectablePosition(0, true); // 正常来说,应该是返回0
setSelectedPositionInt(position);
/*
爷爷类AdapterView里的方法,代码如下
void setSelectedPositionInt(int position) {
mSelectedPosition = position; // 设置mSelectedPosition
mSelectedRowId = getItemIdAtPosition(position); // 设置mSelectedRowId,BaseAdapter一般也就返回的是position
}
*/
sel = fillFromTop(childrenTop); // 从childrenTop开始加载子view
} else {
final int position = lookForSelectablePosition(mItemCount - 1, false);
setSelectedPositionInt(position);
sel = fillUp(mItemCount - 1, childrenBottom);
}
} else { // 不是第一次加载,而且数据源没有改变,就进入下面的逻辑,优先将指定位置的view加载,再加载其他的view
if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) { // mSelectedPosition,没有选中的话就是-1
sel = fillSpecific(mSelectedPosition,
oldSel == null ? childrenTop : oldSel.getTop());
} else if (mFirstPosition < mItemCount) { // 默认是这个分支
sel = fillSpecific(mFirstPosition, // oldFirst默认是listView的第一个子view
oldFirst == null ? childrenTop : oldFirst.getTop());
} else {
sel = fillSpecific(0, childrenTop);
}
}
break;
}
// Flush any cached views that did not get reused above
recycleBin.scrapActiveViews(); // 清除所有没用到的缓存view,把它们移到回收站中
// remove any header/footer that has been temp detached and not re-attached
// 清除没有用到的header或footer
removeUnusedFixedViews(mHeaderViewInfos);
removeUnusedFixedViews(mFooterViewInfos);
// 处理焦点
..
// 无障碍处理
..
.. // 焦点处理
mLayoutMode = LAYOUT_NORMAL;
mDataChanged = false;
.. // 滚动条处理
mNeedSync = false;
setNextSelectedPositionInt(mSelectedPosition);
.. // 滚动条和选中处理
} finally {
if (mFocusSelector != null) {
mFocusSelector.onLayoutComplete();
}
if (!blockLayoutRequests) {
mBlockLayoutRequests = false;
}
}
}
代码很长很复杂,我这里只记录了跟layout相关的步骤,显然,listView的layoutChildren()要分为第一次布局和非第一次布局来分析,那我就分别记录一下,这里我只考虑正序显示
第一次布局
listView的第一次布局,在layoutChildren()里调用的主要方法依次是lookForSelectablePosition()和fillFromTop(),按顺序点进去源码看看
ListView#lookForSelectablePosition
代码如下
@Override
int lookForSelectablePosition(int position, boolean lookDown) {
final ListAdapter adapter = mAdapter;
.. // 合法性判断
final int count = adapter.getCount();
if (!mAreAllItemsSelectable) {
if (lookDown) { // 进入这里
position = Math.max(0, position);
while (position < count && !adapter.isEnabled(position)) {
position++; // 找到第一个在adapter里返回enabled的子view,默认情况就是第一个
}
} else {
position = Math.min(position, count - 1);
while (position >= 0 && !adapter.isEnabled(position)) {
position--;
}
}
}
if (position < 0 || position >= count) {
return INVALID_POSITION;
}
return position;
}
代码不复杂,只是获取了第一个enable的子view的位置
ListView#fillFromTop
代码如下
private View fillFromTop(int nextTop) {
// 更新mFirstPosition,传入的nextTop是childrenTop,也就是listView里子view的顶端极限
mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
if (mFirstPosition < 0) {
mFirstPosition = 0;
} // 第一次布局的话,mFirstPosition为0
return fillDown(mFirstPosition, nextTop);
}
调用了fillDown()方法,代码如下
private View fillDown(int pos, int nextTop) {
// 给ListView遍历添加指定位置之下的view
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
} // 确定listView里子view的最底端
while (nextTop < end && pos < mItemCount) { // 不到底并且不遍历完,所以ListView一次显示后的getChildCount()是可视范围内的子view数目,而不是全部item的数量
// is this the selected item? 似乎第一次加载,默认adapter里第一个view是选中的,也就是方才说的mSelectedPosition = 0
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); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
return selectedView;
}
循环调用了makeAndAddView()方法,代码如下
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) { // flowDown()过来的话,flow是true;flowup()过来的话,flow是false
if (!mDataChanged) { // 第一次加载的话,flow是true,mDataChanged为false
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position); // 第一次加载这里是null,但以后就不是了
if (activeView != null) {
// Found it. We're reusing an existing child, so it just needs
// to be positioned like a scrap view.
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
// 第一次加载要进行obtainView()
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
关于mRecycler.getActiveView()方法,请参见文章安卓开发学习之ListView缓存策略中常见的方法。第一次加载的话,从回收站里获取的view是空,所以要多调用一个obtainView()方法,这个方法的记录,请参加文章安卓开发学习之ListView的测量流程源码阅读,然后调用了setupChild()方法,代码如下
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) {
// 第一次加载,flowDown是true
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(); // 经过onMeasure()的measureScrapChild()后,child.isLayoutRequested()为true
// Respect layout params that are already in the view. Otherwise make
// some up...
// 更新子view的layoutParams
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.
// 更新child的selected和pressed状态
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
// 设置checked或activated
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));
}
}
// 第一次layout:
// 经过onMeasure()的measureScrapChild()后,p.forceAdd = true,所以如果子view不是header或footer的话,走的是else分支
// isAttachedToWindow是mIsScrap[0],这个经过obtainView()后只有从回收站里获取到老的view时,才会是true
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
// 把子view绑定给listView,如果是获取child的下一个view,则是从flowDown()过来的,flowDown为true
// 如果是获取child的上一个view,则是从flowUp()过来的,flowDown为false
// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position) {
// 如果子view的位置和之前回收站里的位置不一样,手动跳到当前状态
child.jumpDrawablesToCurrentState();
}
} else { // 第一次layout走这里
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
// 这里才是真正的给ListView添加子view,最终会调用到ViewGroup.addInArray(child, index)方法,把child放在mChildren的最后
addViewInLayout(child, flowDown ? -1 : 0, p, true);
// add view in layout will reset the RTL properties. We have to re-resolve them
child.resolveRtlPropertiesIfNeeded(); // 重新解析子view的展示方向,从左往右还是相反,此处省略
}
if (needToMeasure) {
// 子view会再测量一次
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());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
最关键的就是在else里,调用了addViewInLayout()方法,把child加到listView中,此后listView的子view数量,不会是0了。
至于子view测量和布局那里,先是调用了ViewGroup.getChildMeasureSpec()方法以获取子view的宽度spec,这个方法的源码阅读参见文章安卓开发学习之View测量的内置常用方法
而后根据子view的layoutParams里的height是否大于0,分别调用Measure.makeMeasureSpec/makeSafeMeasureSpec()方法,传入参数分别是子view的参数高度、精确模式和listView的父view指定的高度、unspecified模式,关于这两个方法,请参见文章安卓开发学习之MeasureSpec
最后调用child.measure()方法进行测量,child.layout()方法进行布局,这俩方法请参见文章Android开发学习之View测量绘制流程源码阅读记录
至此,ListView的第一次布局就完成了
非第一次布局
如果不是第一次布局(不考虑数据源变化),那么就是调用了recycleBin.fillActiveViews()方法和fillSpecific()方法
前者用来缓存当前ListView中的子view,代码参见文章安卓开发学习之ListView缓存策略中常见的方法
后者优先加载listView中指定位置(第一个位置)的子view,然后加载这个子view之前和之后的view(如果有的话),代码如下
private View fillSpecific(int position, int top) {
// 传入参数:mFirstPosition和listView里第一项的顶部
boolean tempIsSelected = position == mSelectedPosition;
View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected); // 先加载指定位置的view
// Possibly changed again in fillUp if we add rows above this one.
mFirstPosition = position;
View above;
View below;
final int dividerHeight = mDividerHeight;
if (!mStackFromBottom) { // 正常顺序显示的话,走这里
above = fillUp(position - 1, temp.getTop() - dividerHeight); // 为ListView添加指定位置之上的view,并获取焦点view
// This will correct for the top of the first view not touching the top of the list
adjustViewsUpOrDown(); // 调整所有子view的高度,保证让最上面或最下面(正常顺序是最上面)子view不悬空
below = fillDown(position + 1, temp.getBottom() + dividerHeight); // 为ListView添加指定位置之下的view,并获取焦点view
int childCount = getChildCount();
if (childCount > 0) {
// 如果listView已经显示完了,处理一下底部的空当
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);
}
}
// 返回焦点view
if (tempIsSelected) {
return temp;
} else if (above != null) {
return above;
} else {
return below;
}
}
按照顺序,调用了makeAndAddView()、fillUp()、adjustViewsUpOrDown()、fillDown()、correctTooHight()方法,我们依次来看
makeAndAddView
之前看过,现在再看一次
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) { // flowDown()过来的话,flow是true;flowup()过来的话,flow是false
if (!mDataChanged) {
// Try to use an existing view for this position.
final View activeView = mRecycler.getActiveView(position); // 不是第一次加载,这里不是null
if (activeView != null) {
// 走这里
setupChild(activeView, position, y, flow, childrenLeft, selected, true);
return activeView;
}
}
// Make a new view for this position, or convert an unused view if
// possible.
// 非第一次加载,这些逻辑不会走了
final View child = obtainView(position, mIsScrap);
// This needs to be positioned and measured.
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
少走一步obtainView(),看来虽然之前detachAllViewsFromParents()把所有子view都清除掉了,但也只是从回收站里重新获取,所以省了很多事。我们再进入setupChild()看看
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
boolean selected, boolean isAttachedToWindow) { // 插入指定位置,flowDown为true
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(); // 经过onMeasure()的measureScrapChild()后,child.isLayoutRequested()为true
// Respect layout params that are already in the view. Otherwise make
// some up...
// 更新子view的layoutParams
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.
// 更新child的selected和pressed状态
if (updateChildSelected) {
child.setSelected(isSelected);
}
if (updateChildPressed) {
child.setPressed(isPressed);
}
// 设置checked或activated
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));
}
}
// 之后的layout:
// 第一次以后的layout,isAttachToWindow直接成了true,p.forceAdd也在第一测量后变成了false,所以走的就是if分支
if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p); // 如果是child本身,flowDown为true
// 把子view绑定给listView,如果是获取child的下一个view,则是从flowDown()过来的,flowDown为true
// 如果是获取child的上一个view,则是从flowUp()过来的,flowDown为false
// If the view was previously attached for a different position,
// then manually jump the drawables.
if (isAttachedToWindow
&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
!= position) {
// 如果子view的位置和之前回收站里的位置不一样,手动跳到当前状态,忽略
child.jumpDrawablesToCurrentState();
}
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
// 这里才是真正的给ListView添加子view,最终会调用到ViewGroup.addInArray(child, index)方法,把child放在mChildren的最后
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) {
// 子view会再测量一次
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());
}
if (mCachingStarted && !child.isDrawingCacheEnabled()) {
child.setDrawingCacheEnabled(true);
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
最关键的是调用了attachViewToParent()方法,因为在layoutChildren()的时侯,把所有子view都detach了,现在就是要attach一下。它的源代码如下
protected void attachViewToParent(View child, int index, LayoutParams params) {
child.mLayoutParams = params;
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// 添加子view,flowDown的话,index为mChildrenCount;flowUp的话,index为0
// 也就是添加到mChildren尾部和头部的区别
child.mParent = this;
child.mPrivateFlags = (child.mPrivateFlags & ~PFLAG_DIRTY_MASK
& ~PFLAG_DRAWING_CACHE_VALID)
| PFLAG_DRAWN | PFLAG_INVALIDATED;
this.mPrivateFlags |= PFLAG_INVALIDATED;
if (child.hasFocus()) {
requestChildFocus(child, child.findFocus());
}
// 处理回调
dispatchVisibilityAggregated(isAttachedToWindow() && getWindowVisibility() == VISIBLE
&& isShown());
}
返回setupChild()后,就跟第一次布局没什么区别了,一路返回到fillSpecific()方法,接着看fillUp()
fillUp
private View fillUp(int pos, int nextBottom) {
// 给ListView遍历添加指定位置及之上的view
View selectedView = null;
int end = 0;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end = mListPadding.top;
}
while (nextBottom > end && pos >= 0) {
// is this the selected item?
boolean selected = pos == mSelectedPosition;
View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
nextBottom = child.getTop() - mDividerHeight;
if (selected) {
selectedView = child;
}
pos--;
}
mFirstPosition = pos + 1; // 更新ListView中第一个子view的位置,是第一个能完整显示的子view
setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
return selectedView;
}
没啥好说的,就是遍历执行makeAndAddView()方法,只不过这里的flowDown为false,也就是在attachViewToParent里调用addInArray()时,采用头插法
adjustViewsUpOrDown
从fillUp()返回后,调用了adjustViewsUpOrDown()来调整所有子view的高度,代码如下
private void adjustViewsUpOrDown() {
final int childCount = getChildCount();
int delta;
if (childCount > 0) { // 此时绝不应该是0
View child;
if (!mStackFromBottom) { // 正常顺序走这里
// Uh-oh -- we came up short. Slide all views up to make them
// align with the top
child = getChildAt(0);
delta = child.getTop() - mListPadding.top; // listView中第一个子view到顶端的距离
if (mFirstPosition != 0) { // 如果listView中第一个子view不是adapter中的第一项
// It's OK to have some space above the first item if it is
// part of the vertical spacing
delta -= mDividerHeight; // 就再减去分割线的距离
}
if (delta < 0) {
// We only are looking to see if we are too low, not too high
delta = 0;
}
} else {
// we are too high, slide all views down to align with bottom
child = getChildAt(childCount - 1);
delta = child.getBottom() - (getHeight() - mListPadding.bottom);
if (mFirstPosition + childCount < mItemCount) {
// It's OK to have some space below the last item if it is
// part of the vertical spacing
delta += mDividerHeight;
}
if (delta > 0) {
delta = 0;
}
}
if (delta != 0) {
// 调整listView中所有子view的顶端和底端
offsetChildrenTopAndBottom(-delta);
}
}
}
最后的offsetChildrenTopAndBottom()方法是ViewGroup里的方法,代码如下
public void offsetChildrenTopAndBottom(int offset) {
final int count = mChildrenCount;
final View[] children = mChildren;
boolean invalidate = false;
for (int i = 0; i < count; i++) {
final View v = children[i];
v.mTop += offset;
v.mBottom += offset;
// 硬件加速,忽略
if (v.mRenderNode != null) {
invalidate = true;
v.mRenderNode.offsetTopAndBottom(offset);
}
}
// 硬件加速,忽略
if (invalidate) {
invalidateViewProperty(false, false);
}
// 通知回调
notifySubtreeAccessibilityStateChangedIfNeeded();
}
原来就是更新了子view的top和bottom
fillDown
调整完后,调用了fillDown()方法,加载指定位置之下的view,代码如下
private View fillDown(int pos, int nextTop) {
// 给ListView遍历添加指定位置之下的view
View selectedView = null;
int end = (mBottom - mTop);
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
end -= mListPadding.bottom;
} // 确定可视范围内子view的最底端
while (nextTop < end && pos < mItemCount) { // 不到底并且不遍历完,所以ListView一次显示后的getChildCount()是可视范围内的子view数目,而不是全部item的数量
// is this the selected item? 似乎第一次加载,默认adapter里第一个view是选中的,也就是方才说的mSelectedPosition = 0
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); // 这里要调用mRemoteAdapter的同名方法,不手动设置mRemoteAdapter的话,就是摆设
return selectedView;
}
也没啥,只不过把flowDown设成true传给了makeAndAddView(),最后在addInArray()中使用了尾插法
correctTooHigh
从flowDown()方法返回后,如果childCount > 0,也就是子view数目不为0,就调用correctTooHigh()方法,处理底部空当,代码如下
private void correctTooHigh(int childCount) {
// First see if the last item is visible. If it is not, it is OK for the
// top of the list to be pushed up.
int lastPosition = mFirstPosition + childCount - 1; // listView中最后一个view的位置
if (lastPosition == mItemCount - 1 && childCount > 0) { // 如果listView已经显示完了
// Get the last child ...
final View lastChild = getChildAt(childCount - 1);
// ... and its bottom edge
final int lastBottom = lastChild.getBottom(); // 最后一个子view的底部
// This is bottom of our drawable area
final int end = (mBottom - mTop) - mListPadding.bottom; // 子view的底部极限
// This is how far the bottom edge of the last view is from the bottom of the
// drawable area
int bottomOffset = end - lastBottom; // 底部偏移量
View firstChild = getChildAt(0); // listView中第一个子view
final int firstTop = firstChild.getTop(); // 第一个子view的顶部
// Make sure we are 1) Too high, and 2) Either there are more rows above the
// first row or the first row is scrolled off the top of the drawable area
if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top)) {
// 底部太高,并且listView中第一个子view不是adapter的第一项,即便是第一项,也太低
if (mFirstPosition == 0) {
// Don't pull the top too far down
// listView中第一个子view是adapter的第一项的话(此时adapter的item太少,不用滑就能显示全部),偏移量bottomOffset
bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
// paddingTop - firstTop < 0 表示listView里第一个子view太靠下了,上面出现空当,此时bottomOffset为负
}
// Move everything down
offsetChildrenTopAndBottom(bottomOffset); // 整体位置向下偏移bottomOffset(不一定真的往下偏移)
if (mFirstPosition > 0) {
// Fill the gap that was opened above mFirstPosition with more rows, if
// possible
// 如果listView中第一个子view不是adapter的第一项,也就是发生了滑动,就把上面的空余部分补齐
fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight);
// Close up the remaining gap
adjustViewsUpOrDown(); // 如果是正序显示,并且把上面空余部分补齐后,最上方又出现空当,就补齐上面的空当。如果是逆序显示,就补齐下面的空当
}
}
}
}
注释已经写得很清楚了,这样fillSpecific()就执行完了,非第一次布局也就结束了。
至于layoutChildren()剩下的一些和缓存有关的方法(比如recycleBin.removeSkippedScrap()、recycleBin.scrapActiveViews()等等),参见文章安卓开发学习之ListView缓存策略中常见的方法
结语
ListView的布局流程大体就这样,很复杂,值得反复品味。最后关于listView的绘制流程,请参见文章安卓开发学习之ListView的绘制流程源码阅读