背景
这几天我在看Android测绘方面的源码,今日突发奇想,想看看ListView的测绘流程,所以就从onMeasure()开始,进行阅读
ListView#onMeasure
ListView的onMeasure()方法源码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
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(); // adapter里的子项数量
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap); // 获取子view,由于这里还没有addView(),所以这时只能从adapter里获取第1项子view
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
measureScrapChild(child, 0, widthMeasureSpec, heightSize); // 测量子view
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0); // 这里的判断条件:前者默认就是true,后者只要子view的viewType>=0就是true,而viewType默认就是0
}
}
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
先是调用了父类AbsListView的onMeasure()方法,代码如下
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mSelector == null) {
useDefaultSelector(); // 设置selector
}
// 设置列表的内间距
final Rect listPadding = mListPadding;
listPadding.left = mSelectionLeftPadding + mPaddingLeft;
listPadding.top = mSelectionTopPadding + mPaddingTop;
listPadding.right = mSelectionRightPadding + mPaddingRight;
listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
// Check if our previous measured size was at a point where we should scroll later.
if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { // 这个判断一般会进来
final int childCount = getChildCount();
final int listBottom = getHeight() - getPaddingBottom(); // list的底部
final View lastChild = getChildAt(childCount - 1);
final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; // 最后一个的子view的底部
mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount &&
lastBottom <= listBottom;
// 是否需要强制滚到底部,为true的条件:前一个分支:mLastHandledItemCount被赋值成了itemCount,所以条件就是当前列表显示的第一个位置 + 子view数目 >= itemCount
// 后一个分支:最后一个子view的底部<=列表显示的底部,也就是最后一个子view显示完全或腾空了
}
}
很简单,返回ListView.onMeasure()后,进入判断:如果itemCount正常>0,并且父view给listView的宽高指示有一个是unspecified,就调用obtainView()方法获取子项view,这里只会获取第一项,因为在这里还没有进行addView()操作,ListView里不会有子view,只能通过adapter获取第一项子view的高度,以便至少显示一项子view。
AbsListView#obtainView
那我们进入obtainView()方法,这是父类AbsListView的方法,源码如下
View obtainView(int position, boolean[] outMetadata) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
outMetadata[0] = false; // outMetadata[0]的真假性表示当前view是否从回收站里回收出来的
// Check whether we have a transient state view. Attempt to re-bind the
// data and discard the view if we fail.
final View transientView = mRecycler.getTransientStateView(position); // 从回收站中获取这个位置的短暂标记的view(标志位标记,一般为null)
if (transientView != null) {
final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
// If the view type hasn't changed, attempt to re-bind the data.
if (params.viewType == mAdapter.getItemViewType(position)) { // view的type没有变的话,重新绑定数据
final View updatedView = mAdapter.getView(position, transientView, this); // 调用adapter.getView()方法,绑定数据
// If we failed to re-bind the data, scrap the obtained view.
if (updatedView != transientView) {
setItemViewLayoutParams(updatedView, position); // 为新view检查并设置layoutParams
mRecycler.addScrapView(updatedView, position); // 更新回收站里的这个位置的view
}
}
outMetadata[0] = true;
// Finish the temporary detach started in addScrapView().
transientView.dispatchFinishTemporaryDetach();
return transientView;
}
// 正常情况下,回收站里没有这个位置上的transiantView
final View scrapView = mRecycler.getScrapView(position); // 尝试从回收站中获取这个位置的view
final View child = mAdapter.getView(position, scrapView, this); // 还是调用adapter的getView()方法
if (scrapView != null) {
if (child != scrapView) { // 如果这个view和从回收站里拿出来的不是一个东西,就说明这个view是这个位置上的第一个view
// Failed to re-bind the data, return scrap to the heap.
mRecycler.addScrapView(scrapView, position); // 更新回收站中这个位置的view
} else if (child.isTemporarilyDetached()) { // 是一回事,并且只是暂时和list分离了
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);
}
setItemViewLayoutParams(child, position); // 检查并设置layoutParams
if (AccessibilityManager.getInstance(mContext).isEnabled()) { // 无障碍处理,很少用到
if (mAccessibilityDelegate == null) {
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
}
if (child.getAccessibilityDelegate() == null) {
child.setAccessibilityDelegate(mAccessibilityDelegate);
}
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return child;
}
我们不关注transientView的情况,直接从下面的mRecycler.getScrapView()开始看,这个方法请参见文章安卓开发学习之ListView缓存策略中常见的方法
然后我们一路返回,返回到AbsListView#obtainView(),继续往下读,如果我们从adapter里获取的view和从回收站里获取的view不是一码事,那就把从adapter里获取的view添加进回收站,调用的是mRecycler.addScrapView(),代码参见文章安卓开发学习之ListView缓存策略中常见的方法
好,然后再返回到AbsListView中,这个方法里最后一个比较重要的方法就是setItemViewLayoutParams(),这是用来设置view的布局参数信息,这个LayoutParams是AbsListView自己的LayoutParams,继承了View.LayoutParams,里面添加了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) { // mAdapterHasStableIds变量,如果adapter继承BaseAdapter的话,默认为false
lp.itemId = mAdapter.getItemId(position);
}
lp.viewType = mAdapter.getItemViewType(position);
lp.isEnabled = mAdapter.isEnabled(position); // BaseAdapter默认是true
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
如果子view的LayoutParams是空,AbsListView调用generateDefaultLayoutParams()方法生成一个宽度match_parent,高度wrap_content,view_type是0的AbsListView.LayoutParams对象
如果子view的LayoutParams不是AbsListView.LayoutParams类型的,就调用generateLayoutParams()方法根据子view的布局参数信息new一个AbsListView.LayoutParams对象
然后进行viewType和isEnabled的赋值后,必要的话,重新设置子view的布局参数
完事后,AbsListView.obtainView()方法就差不多了。
ListView#measureScrapChild
返回到ListView.onMeasure()后,执行measureScrapChild()进行子view的测量,代码如下
private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
LayoutParams p = (LayoutParams) child.getLayoutParams();
if (p == null) { // 如果子view没有layoutParam,就构造一个宽高都是内容包裹的AbsListvView.LayoutParams给它
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
child.setLayoutParams(p);
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
p.forceAdd = true;
final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
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(heightHint, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
// Since this view was measured directly aginst the parent measure
// spec, we must measure it again before reuse.
child.forceLayout(); // 设置标志位
}
先是调用了ViewGroup.getChildMeasureSpec()方法以获取子view的宽度spec,这个方法的源码阅读参见文章安卓开发学习之View测量的内置常用方法
而后根据子view的layoutParams里的height是否大于0,分别调用Measure.makeMeasureSpec/makeSafeMeasureSpec()方法,传入参数分别是子view的参数高度、精确模式和listView的父view指定的高度、unspecified模式,关于这两个方法,请参见文章安卓开发学习之MeasureSpec
最后调用child.measure()方法进行测量,这个方法请参见文章Android开发学习之View测量绘制流程源码阅读记录
然后设置个标志位,就返回了ListView.onMeasure()。
ListView#measureHeightOfChildren
之后进入一个比较容易出问题的一步,根据heightMode还决定ListView的高度是否只有一个子view的高度,这个如果父view是scrollView的话,heightMode会是unspecified,所以由ScrollView包裹的ListView,只会显示第一项,而网上的解决方案是自定义view继承ListView,覆写onMeasure(),把heightMode设为at_most,原因就在这儿,强制调用measureHeightOfChildren()方法,获取正确的高度,这个方法源代码如下
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
// widthMeasureSpec, 0, NO_POSITION, heightSize, -1
final ListAdapter adapter = mAdapter;
if (adapter == null) { // 适配器为空,就不存在子view了
return mListPadding.top + mListPadding.bottom;
}
// Include the padding of the list
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; // 一般情况就是adapter.count() - 1
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure(); // true
final boolean[] isScrap = mIsScrap;
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap); // 这里就是获取每一个子view了
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1); // 这里每一个子view的回收站位置信息都是-1,所以这种情况下,AbsListView.obtainView()中从回收站获取的view,都是最近被回收的view
}
returnedHeight += child.getMeasuredHeight(); // 累积高度
if (returnedHeight >= maxHeight) { // 如果子view的高度和已经不小于父view指定的高度了,返回的是父view指定的高度,而不是子view高度之和
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely.
return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight; // 这里会返回maxHeight,而不是子view高度之和
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) { // 这里进不来
prevHeightWithoutPartialChild = returnedHeight;
}
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight; // 子view高度之和小于父view指定的高度,方可返回子view高度之和
}
注释写的很清晰,我就不过多解释了
结语
关于ListView的测量流程就看到这儿,关于它的布局流程,比较复杂,参见这篇文章安卓开发学习之ListView的布局流程源码阅读