ListView工作原理分析
ListView可谓是我们在Android开发中使用频率最高的组件之一了,并且,相比TextView、Buton等组件而言,ListView的使用也更复杂,因此,了解ListView的工作原理将使得我们能更好的使用ListView。此外,ListView一般用来以列表形式展示数据,但是在某些情况下,ListView要处理的数据可能多达数万条,但是并不会发生OOM,这也是ListView的特别之处,本篇文章将一步步讲解ListView的工作原理,揭开ListView神秘的面纱。
ListView是AbsListView的子类,AbsListView继承自AdapterView,而AdapterView又继承自ViewGroup,因此,无论ListView如何特别,它也是一个ViewGroup。因此,ListView也要实现自己的traversal过程,也即measure、layout以及draw,measure和draw过程都没有什么特别的,layout过程相对而言比较复杂。此外,ListView还要处理滑动事件,在用户滑动屏幕时进行View的切换。下面我们就一块来看一下ListView的traversal过程以及滑动事件的处理。
RecyclerBin
在分析ListView之前,不得不先了解一下RecyclerBin。RecyclerBin是ListView展示成千上万条数据而不发生OOM的根本所在。RecyclerBin是AbsListView的内部类,它负责View的复用,View在RecycleBin中分两级存储:ActiveViews和ScrapViews。ActiveViews是在进行layout之前正在屏幕上显示的View,layout之后会被放入到ScrapViews中,ActiveViews在存储在数组中;而ScrapViews被无序存储在ArrayList中,它被Adapter使用,当调用Adapter的getView方法时,会从ScrapViews中拿出一个View作为convertView传递给getView,避免分配过多不必要的View。
下面看一下RecyclerBin中主要的方法:
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();
// Don't put header or footer views into the scrap heap
if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
// However, we will NOT place them into scrap views.
activeViews[i] = child;
// Remember the position so that setupChild() doesn't reset state.
lp.scrappedFromPosition = firstActivePosition + i;
}
}
}
View getActiveView(int position) {
int index = position - mFirstActivePosition;
final View[] activeViews = mActiveViews;
if (index >=0 && index < activeViews.length) {
final View match = activeViews[index];
activeViews[index] = null;
return match;
}
return null;
}
void scrapActiveViews() {
final View[] activeViews = mActiveViews;
final boolean hasListener = mRecyclerListener != null;
final boolean multipleScraps = mViewTypeCount > 1;
ArrayList<View> scrapViews = mCurrentScrap;
final int count = activeViews.length;
for (int i = count - 1; i >= 0; i--) {
final View victim = activeViews[i];
if (victim != null) {
final AbsListView.LayoutParams lp
= (AbsListView.LayoutParams) victim.getLayoutParams();
final int whichScrap = lp.viewType;
activeViews[i] = null;
if (victim.hasTransientState()) {
// Store views with transient state for later use.
victim.dispatchStartTemporaryDetach();
if (mAdapter != null && mAdapterHasStableIds) {
if (mTransientStateViewsById == null) {
mTransientStateViewsById = new LongSparseArray<View>();
}
long id = mAdapter.getItemId(mFirstActivePosition + i);
mTransientStateViewsById.put(id, victim);
} else if (!mDataChanged) {
if (mTransientStateViews == null) {
mTransientStateViews = new SparseArray<View>();
}
mTransientStateViews.put(mFirstActivePosition + i, victim);
} else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
// The data has changed, we can't keep this view.
removeDetachedView(victim, false);
}
} else if (!shouldRecycleViewType(whichScrap)) {
// Discard non-recyclable views except headers/footers.
if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(victim, false);
}
} else {
// Store everything else on the appropriate scrap heap.
if (multipleScraps) {
scrapViews = mScrapViews[whichScrap];
}
lp.scrappedFromPosition = mFirstActivePosition + i;
removeDetachedView(victim, false);
scrapViews.add(victim);
if (hasListener) {
mRecyclerListener.onMovedToScrapHeap(victim);
}
}
}
}
pruneScrapViews();
}
View getScrapView(int position) {
final int whichScrap = mAdapter.getItemViewType(position);
if (whichScrap < 0) {
return null;
}
if (mViewTypeCount == 1) {
return retrieveFromScrap(mCurrentScrap, position);
} else if (whichScrap < mScrapViews.length) {
return retrieveFromScrap(mScrapViews[whichScrap], position);
}
return null;
}
- fillActiveViews方法会将ListView中出了Footer和Header之外的所有的View都添加到mActiveViews中;
- getView方法用于从mActiveViews中获取一个View,注意,获取之后立即从mActiveViews中移除;
- scrapActiveViews方法将mActiveViews中的View全部移动到mCurrentScrap中;
- getScrapView方法从mCurrentScrap中获取一个View(whichScrap默认是0),获取的View是与position相同的一个或者是最后一个;
Measure过程
ListView的measure过程没有什么特别之处,毕竟ListView也是View,遵循继承ViewGroup实现自定义View的套路,而且,大多时候ListView都是match_parent。下面直接看一下onMeasure方法:
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();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final 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) {
// 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;
}
该方法中SpecMode一般不为UNSPECIFIED,因此childState也总是0,那么ListView的width即为widthSize,也即是MATCH_PARENT的效果;而如果heightMode为AT_MOST,那么高度为heightSize 与数据集中元素个数 * 子View高度 中的最小值:
- 如果ListView的width为wrap_content,则与match_parent效果相同;
- 如果ListView的height为wrap_content,则高度为min(heightSize, 数据集中元素个数 * 子View高度);
Layout过程
相比之下,layout过程要稍显复杂。ListView的layout过程实现在父类AbsListView中,看一下AbsListView中的onLayout方法:
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();
}
layoutChildren();
mInLayout = false;
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
// TODO: Move somewhere sane. This doesn't belong in onLayout().
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
}
可以看到,onLayout方法只是简单的将layout逻辑交给了layoutChildren来处理,而layoutChildren实现为空,很明显,layoutChildren方法是为了让子类来实现自己的布局逻辑。ListView中的layoutChildren方法比较长,因此我对layoutChildren方法进行了精简,只留下了布局逻辑相关的部分,下面我们来看一下精简后的layoutChildren方法:
protected void layoutChildren() {
……//super.layoutChildren()
final int childrenTop = mListPadding.top;
final int childrenBottom = mBottom - mTop - mListPadding.bottom;
final int childCount = getChildCount(); //1
View sel;
……//记录选中的View、处理数据集为空的情况、记录有访问性焦点的View
final int firstPosition = mFirstPosition;
final RecycleBin recycleBin = mRecycler;
if (dataChanged) {
for (int i = 0; i < childCount; i++) { //2
recycleBin.addScrapView(getChildAt(i), firstPosition+i);
}
} else {
recycleBin.fillActiveViews(childCount, firstPosition); //3
}
detachAllViewsFromParent(); //4
recycleBin.removeSkippedScrap();
switch (mLayoutMode) {
……//其它几个case,默认mLayoutMode为LAYOUT_DEFAULT,对应default情况
default:
if (childCount == 0) {
if (!mStackFromBottom) {
final int position = lookForSelectablePosition(0, true);
setSelectedPositionInt(position);
sel = fillFromTop(childrenTop); //5
} 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;
}
recycleBin.scrapActiveViews();
……//移除为使用的Header和Footer、处理View焦点等问题
}
由于ListView比较特殊,前后两次layout过程差别比较大,因此我们将分别分析第一次、第二次layout 过程。
首先,在进行第一次layout时,还没有View被添加到ListView中,因此1处的childCount为0,而dataChanged也只有在数据集改变时才会为true,因此会执行3处fillActiveViews方法。如前所述,该方法会将显示的View添加到mActiveViews中,但由于此时childCount为0,因此并不会产生什么影响,同样4处的detachAllViewsFromParent方法也不会产生任何影响。
然后,默认布局是从上往下,因此会执行5处的fillFromTop方法,fillFromTop方法里又调用了fillDown,因此我们看一下fillDown方法,参数childrenTop也即mListPadding.top,也即是第一个子View开始的位置:
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;
}
while (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;
}
fillDown方法的逻辑很简单,它以nextTop作为下一个View开始的位置,然后通过makeAndAddView添加一个View,并增加nextTop,直到填满ListView或者加载了所有item,也即nextTop >= end || pos >= mItemCount。那么,不难发现,该方法中的makeAndAddView方法大有玄机。
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
boolean selected) {
View child;
if (!mDataChanged) {
// Try to use an existing view for this position
child = mRecycler.getActiveView(position);
if (child != null) {
// Found it -- we're using an existing child
// This just needs to be positioned
setupChild(child, position, y, flow, childrenLeft, selected, true);
return child;
}
}
// Make a new view for this position, or convert an unused view if possible
child = obtainView(position, mIsScrap);
// This needs to be positioned and measured
setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
return child;
}
首先,如果数据集没有改变,makeAndAddView方法通过RecyclerBin的getActiveView方法获得一个View,然后通过setupChild方法添加View;如果数据集已经改变,getActiveView返回了null,则通过obtainView方法获取View,然后调用setupChild。
显然,第一次layout时,getActiveView一定返回null,因此child会通过obtainView方法获取。下面看一下obtainView方法:
View obtainView(int position, boolean[] isScrap) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
isScrap[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 scrapView = mRecycler.getScrapView(position);
final 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()) {
isScrap[0] = true;
// Finish the temporary detach started in addScrapView().
child.dispatchFinishTemporaryDetach();
} else {
isScrap[0] = false;
}
}
}
……//
return child;
}
首先尝试从RecyclerBin中获取一个ScrapView,然后通过mAdapter.getView方法重新为该View绑定数据来复用该View,如果重新绑定数据失败(也即mAdapter.getView返回的View != 传入的scrapView)就会将获得的ScrapView重新放入mScrapViews中。
到这里,不知道大家有没有看到一些熟悉的东西!对,这里的getView就是我们继承BaseAdapter时重写的getView方法,而传入的scrapView就是参数convertView。现在看一个我们实现getView的简单示例:
public View getView(int position, View convertView, ViewGroup parent) {
View view;
if (convertView == null) {
view = LayoutInflater.from(getContext()).inflate(resourceId, null);
} else {
view = convertView;
}
((TextView)view.findViewById(R.id.text)).setText(getItem(position));
return view;
}
可以看到,View的复用就是在getView中实现的,判断convertView是否为null,如果convertView不为null,则为该View重新绑定数据,复用该View。(其实在通过getScrapView获取复用View之前会先尝试获取一个具有transient状态的View,具体逻辑与上面相同。)
由于是第一次layout,因此此时convertView都为null,也即会通过LayoutInflater创建新的View来填满ListView。
回到makeAndAddView中,在通过obtainView获取到View之后,会通过setupView来添加View,看一下setupView方法:
private void setupChild(View child, int position, int y, boolean flowDown,
int childrenLeft, boolean selected, boolean recycled) {
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 = !recycled ||
updateChildSelected || child.isLayoutRequested();
// Respect layout params that are already in the view. Otherwise make some up...
// noinspection unchecked
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
p.isEnabled = mAdapter.isEnabled(position);
if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter
&& p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
attachViewToParent(child, flowDown ? -1 : 0, p);
} else {
p.forceAdd = false;
if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
p.recycledHeaderFooter = true;
}
addViewInLayout(child, flowDown ? -1 : 0, p, true);
}
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 (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);
}
if (recycled && (((AbsListView.LayoutParams)
child.getLayoutParams()).scrappedFromPosition)
!= position) {
child.jumpDrawablesToCurrentState();
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
setupChild方法比较长,但是逻辑并不复杂。首先,使用addViewInLayout方法(如果不是一个新创建的View,则会调用attachViewToParent方法将View重新attach到ListView)在onLayout期间将child添加到ListView中,然后如果需要measure的话对child进行measure(在第一次layout期间,由于所有的View都是新创建的,所以都需要measure)。然后调用child.layout()方法将child放到指定位置(如果不是一个新的View,则只需要通过调用offsetLeftAndRight、offsetTopAndBottom来对child进行偏移)。
至此为止,第一次layout过程分析完了,大致流程如下:
- 执行recycleBin.fillActiveViews()方法,将所有child添加到mActiveViews中,但childCount为0,无影响;
- detachAllViewsFromParent()将所有子View从ListView上detach,同样由于childCount为0,无影响;
- fillTopDown -> fillDown,在fillDown中通过makeAndAddView循环获取并添加View来填充ListView;
- makeAndAddView中首先尝试调用mRecycler.getActiveView从RecyclerBin中获取View复用,但此时一定返回null,然后通过obtainView获取View,通过setupView添加View;
- obtainView首先尝试从mScrapViews中获取View复用,但同样此时回返回null,然后调用Adapter的getView(position, null, this)方法获取View,此时getView方法中会通过LayoutInflater来创建新的View;
- setupView中会通过addViewInLayout将child添加到ListView中,由于此时child时新创建的,所有会对child进行measre,然后为child进行layout;
- ListView填充满之后,调用recycleBin.scrapActiveViews方法将mActiveViews中的View移动到mScrapView中,同样,此次layout过程中不产生影响;
- 完成。
对着上面的layoutChildren的代码,我们再看一下在第二次layout过程中会进行哪些活动,首先,可以确定的是,由于第一次layout过程中会在getView中来创建新的View填满ListView,因此第二次layout过程中childCount肯定不为0:
- 执行recycleBin.fillActiveViews()方法,将所有child添加到mActiveViews中,此时mActiveViews一定不为空;
- detachAllViewsFromParent()将所有子View从ListView上detach;
- fillTopDown -> fillDown,在fillDown中通过makeAndAddView循环获取并添加View来填充ListView;
- makeAndAddView中通过recycleBin.getActiveView(),此时getActiveView()返回不为空;
- 将activeView传递给setupView进行复用;
- setupView中重新attach该View,然后对View进行layout;
- 调用recycleBin.scrapActiveViews方法将mActiveViews中为使用的View移动到mScrapView中,以便下次复用;
- 完成;
在两次layout中比较特殊的是没有发生滑动,因此通过fillActiveViews填充的mActiveViews正好能够通过getActiveView重新填充回ListView,此时复用的View全部来源于一级缓存mActiveViews中,因此不需要重新绑定数据,而在最后调用recycleBin.scrapActiveViews也不产生影响,因为此时mActiveViews中的View已经全部被复用了。
但是,如果是通过由于产生滑动而进行layout,那么,由于可能有View被滑出ListView,因此mActiveViews中被滑出的View将不能被直接复用,而是在最后被移动到mScrapView中,因此不能全部填充ListView,此时就会复用二级缓存mScrapViews中的View,并在getView中绑定,如果二级缓存也没有View,那么就会在getView中创建一个View。
OnTouchEvent
onTouchEvent实现在AbsListView中,在onTouchEvent中会在接收到Move事件时调用onTouchMove,而onTouchMove又会调用scrollIfNeeded(),而具体的逻辑就实现在scrollIfNeeded方法中,该方法比较长,我们只需要关心下面的部分:
……//
if (incrementalDeltaY != 0) {
// Coming back to 'real' list scrolling
if (mScrollY != 0) {
mScrollY = 0;
invalidateParentIfNeeded();
}
trackMotionScroll(incrementalDeltaY, incrementalDeltaY);
……//
}
……//
这里我们需要注意trackMotionScroll方法,在该方法中完成了对子View的处理,下面看一下该方法;
……//
if (down) {
int top = -incrementalDeltaY;
if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
top += listPadding.top;
}
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getBottom() >= top) {
break;
} else {
count++;
int position = firstPosition + i;
if (position >= headerViewsCount && position < footerViewsStart) {
// The view will be rebound to new data, clear any
// system-managed transient state.
child.clearAccessibilityFocus();
mRecycler.addScrapView(child, position);
}
}
}
} else {
……//与down的逻辑相同
}
mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
mBlockLayoutRequests = true;
if (count > 0) {
detachViewsFromParent(start, count);
mRecycler.removeSkippedScrap();
}
offsetChildrenTopAndBottom(incrementalDeltaY);
if (down) {
mFirstPosition += count;
}
final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
fillGap(down);
}
……//
可以看到,trackMotionScroll方法中将移出了ListView的child直接加到mScrapViews中,并将它们从ListView中detach,然后调用ViewGroup的offsetChildrenTopAndBottom方法完成children的偏移操作,然后更改mFirstPosition的值,因为mFirstPosition指示了显示的第一个View在数据集中对应的位置,有View移出了ListView,该值也应相应的改变。最后,由于有View滑出了ListView,所以要通过fillGap方法来填充空白,fillGap实现在ListView中,最终也是通过fillDown来复用View进行填充。
同样,如果滑动没有造成child移出ListView,那么,count = 0,只需要通过offsetChildrenTopAndBottom方法偏移children即可。