背景
在安卓开发学习之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的绘制流程源码阅读