ListView是谷歌官方的一个自定义组件,用于列表展示,其中最重要的是Adapter设配器,设配器模式的设计为它带来了极大的性能提升,一方面,内存中只有我们看的到的ItemView被创建(对比ScrollView:有多少子控件就内存中创建多少子控件),另一方面,对ItemView缓存,以便滑动时复用
既然ListView是自定义组件,我们首先找到它的Measure方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// Sets up mListPaddingsuper.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;//获取适配器中的count数(item个数)mItemCount = mAdapter == null ? 0 : mAdapter.getCount();//如果widthMode或者heightMode是UNSPECIFIED模式的话(被其他滑动组件包含),执行下面代码if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED|| heightMode == MeasureSpec.UNSPECIFIED)) {//obtainView方法实例化一个View,obtainView调用的父类的方法,注意:只有一个Viewfinal View child = obtainView(0, mIsScrap);// Lay out child directly against the parent measure spec so that// we can obtain exected minimum width and height.//测量子控件measureScrapChild(child, 0, widthMeasureSpec, heightSize);childWidth = child.getMeasuredWidth();childHeight = child.getMeasuredHeight();childState = combineMeasuredStates(childState, child.getMeasuredState());//加入回收池if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {mRecycler.addScrapView(child, 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) {//如果是wrap_content,执行measureHeightOfChildren方法// TODO: after first layout we should maybe start at the first visible position, not 0heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);}setMeasuredDimension(widthSize, heightSize);mWidthMeasureSpec = widthMeasureSpec;}
我们发现在onMeasure方法中,为什么ScrollView嵌套ListView,ListView只能显示一个item的原因,另外如果ListView的高度设置是wrap_content时,将调用measureHeightOfChildren方法,并且第三个参数是NO_POSITION
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;}// Include the padding of the listint returnedHeight = mListPadding.top + mListPadding.bottom;final int dividerHeight = mDividerHeight;// The previous height value that was less than maxHeight and contained// no partial childrenint prevHeightWithoutPartialChild = 0;int i;View child;// mItemCount - 1 since endPosition parameter is inclusive//在onMeasure中如果高度设置的是wrap_content,则endPosition为NO_POSITION,即endPosition = adapter.getCount() - 1endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;final AbsListView.RecycleBin recycleBin = mRecycler;final boolean recyle = recycleOnMeasure();final boolean[] isScrap = mIsScrap;for (i = startPosition; i <= endPosition; ++i) {//调用了obtainView返回一个Viewchild = obtainView(i, isScrap);//测量measureScrapChild(child, i, widthMeasureSpec, maxHeight);if (i > 0) {// Count the divider for all but one childreturnedHeight += dividerHeight;}// Recycle the view before we possibly return from the method//加入回收池if (recyle && recycleBin.shouldRecycleViewType(((LayoutParams) child.getLayoutParams()).viewType)) {recycleBin.addScrapView(child, -1);}returnedHeight += child.getMeasuredHeight();//判断是否超出最大高度if (returnedHeight >= maxHeight) {// 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;}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 returnedHeightreturn returnedHeight;}
measureHeightOfChildren用于测量ListView的高(wrap_content下),如果所有item的高度之和小于测量建议值,则使用item的高度之和,反之,用建议值。onMeasure和measureHeightOfChildren方法都调用了obtainView方法,我们在AbsListView中找到obtainView方法
View obtainView(int position, boolean[] outMetadata) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");outMetadata[0] = false;// 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);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)) {final View updatedView = mAdapter.getView(position, transientView, this);// If we failed to re-bind the data, scrap the obtained view.if (updatedView != transientView) {setItemViewLayoutParams(updatedView, position);mRecycler.addScrapView(updatedView, position);}}outMetadata[0] = true;// Finish the temporary detach started in addScrapView().transientView.dispatchFinishTemporaryDetach();return transientView;}//先从回收池中得到一个scrapView,可能为空final View scrapView = mRecycler.getScrapView(position);//又调用了adapter的getView方法,将scrapView传过去,其实scrapView就是我们在getView方法中用来复用的convertViewfinal View child = mAdapter.getView(position, scrapView, this);if (scrapView != null) {if (child != scrapView) {// Failed to re-bind the data, return scrap to the heap.mRecycler.addScrapView(scrapView, position);} else if (child.isTemporarilyDetached()) {outMetadata[0] = true;// Finish the temporary detach started in addScrapView().child.dispatchFinishTemporaryDetach();}}if (mCacheColorHint != 0) {child.setDrawingCacheBackgroundColor(mCacheColorHint);}if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);}setItemViewLayoutParams(child, position);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;}
这就是ListView的复用机制一部分(回收池),我们再看onLayout方法,在ListView中并没有发现,在父类AbsListView中发现了
protected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);mInLayout = true;final int childCount = getChildCount();if (changed) {for (int i = 0; i < childCount; i++) {getChildAt(i).forceLayout();}mRecycler.markChildrenDirty();}//ListView改写了layoutChildren方法layoutChildren();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;}
而ListView改写了layoutChildren方法,下面只保留了关键代码
protected void layoutChildren() {
...//mLayoutMode一般都为LAYOUT_NORMAL,所以走defaultswitch (mLayoutMode) {case LAYOUT_SET_SELECTION:if (newSel != null) {sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);} else {sel = fillFromMiddle(childrenTop, childrenBottom);}break;case LAYOUT_SYNC:sel = fillSpecific(mSyncPosition, mSpecificTop);break;case LAYOUT_FORCE_BOTTOM:sel = fillUp(mItemCount - 1, childrenBottom);adjustViewsUpOrDown();break;case LAYOUT_FORCE_TOP:mFirstPosition = 0;sel = fillFromTop(childrenTop);adjustViewsUpOrDown();break;case LAYOUT_SPECIFIC:final int selectedPosition = reconcileSelectedPosition();sel = fillSpecific(selectedPosition, mSpecificTop);/*** When ListView is resized, FocusSelector requests an async selection for the* previously focused item to make sure it is still visible. If the item is not* selectable, it won't regain focus so instead we call FocusSelector* to directly request focus on the view after it is visible.*/if (sel == null && mFocusSelector != null) {final Runnable focusRunnable = mFocusSelector.setupFocusIfValid(selectedPosition);if (focusRunnable != null) {post(focusRunnable);}}break;case LAYOUT_MOVE_SELECTION:sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);break;default:if (childCount == 0) {if (!mStackFromBottom) {final int position = lookForSelectablePosition(0, true);setSelectedPositionInt(position);sel = fillFromTop(childrenTop);} else {final int position = lookForSelectablePosition(mItemCount - 1, false);setSelectedPositionInt(position);sel = fillUp(mItemCount - 1, childrenBottom);}} else {//childCount不为0时执行下面代码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;}
...}
childCount目前是等于0的,并且默认的布局顺序是从上往下,因此会进入到fillFromTop()方法
private View fillFromTop(int nextTop) {mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);if (mFirstPosition < 0) {mFirstPosition = 0;}return fillDown(mFirstPosition, nextTop);}
又调用了fillDown方法
private View fillDown(int pos, int nextTop) {View selectedView = null;int end = (mBottom - mTop);if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {end -= mListPadding.bottom;}//遍历,并且nextTop < end条件至关重要,只会遍历到我们看的见的Viewwhile (nextTop < end && pos < mItemCount) {// is this the selected item?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);return selectedView;}
其中nextTop < end判断,使得ListView只加载在屏幕上的item,我们再来到makeAndAddView方法
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,boolean selected) {if (!mDataChanged) {// Try to use an existing view for this position.final View activeView = mRecycler.getActiveView(position);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.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复用控件,并调用了setupChild方法,最终在setupChild方法中找到了
child.layout(childrenLeft, childTop, childRight, childBottom);
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,boolean selected, boolean isAttachedToWindow) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");final boolean isSelected = selected && shouldShowSelector();final boolean updateChildSelected = isSelected != child.isSelected();final int mode = mTouchMode;final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL&& mMotionPosition == position;final boolean updateChildPressed = isPressed != child.isPressed();final boolean needToMeasure = !isAttachedToWindow || updateChildSelected|| child.isLayoutRequested();// Respect layout params that are already in the view. Otherwise make// some up...AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();if (p == null) {p = (AbsListView.LayoutParams) generateDefaultLayoutParams();}p.viewType = mAdapter.getItemViewType(position);p.isEnabled = mAdapter.isEnabled(position);// Set up view state before attaching the view, since we may need to// rely on the jumpDrawablesToCurrentState() call that occurs as part// of view attachment.if (updateChildSelected) {child.setSelected(isSelected);}if (updateChildPressed) {child.setPressed(isPressed);}if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {if (child instanceof Checkable) {((Checkable) child).setChecked(mCheckStates.get(position));} else if (getContext().getApplicationInfo().targetSdkVersion>= android.os.Build.VERSION_CODES.HONEYCOMB) {child.setActivated(mCheckStates.get(position));}}if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {attachViewToParent(child, flowDown ? -1 : 0, p);// If the view was previously attached for a different position,// then manually jump the drawables.if (isAttachedToWindow&& (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)!= position) {child.jumpDrawablesToCurrentState();}} else {p.forceAdd = false;if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {p.recycledHeaderFooter = true;}addViewInLayout(child, flowDown ? -1 : 0, p, true);// add view in layout will reset the RTL properties. We have to re-resolve themchild.resolveRtlPropertiesIfNeeded();}if (needToMeasure) {final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,mListPadding.left + mListPadding.right, p.width);final int lpHeight = p.height;final int childHeightSpec;if (lpHeight > 0) {childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);} else {childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),MeasureSpec.UNSPECIFIED);}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);}