Layoutmanager负责对recyclerview中的item, measure和layout, 也决定着回收那些不可见的item。支持list,grids和staggered grids的集合,本文主要分析下Linear和Grid的measure和layout过程。
LinearLayoutManager-onLayoutChildren
如果supportsPredictiveItemAnimations返回true, onLayoutchildren将会被执行2次。第一次pre-layout允许记录item的位置,即使这些item被移除,从scrap返回也会被重新放置,帮助计算其他的item位置。第二次是real-layout, 这次仅仅non-removed的views会被使用。如果一个view在pre-layout,但不在real-layout阶段,就会执行DISAPPEARING。看下LinearLayoutManager的具体实现。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
// LLM may decide to layout items for "extra" pixels to account for scrolling target,
// caching or predictive animations.
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
calculateExtraLayoutSpace(state, mReusableIntPair);
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// if the child is visible and we are going to move it around, we should layout
// extra items in the opposite direction to make sure new items animate nicely
// instead of just fading in
final View existing = findViewByPosition(mPendingScrollPosition);
if (existing != null) {
final int current;
final int upcomingOffset;
if (mShouldReverseLayout) {
current = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(existing);
upcomingOffset = current - mPendingScrollPositionOffset;
} else {
current = mOrientationHelper.getDecoratedStart(existing)
- mOrientationHelper.getStartAfterPadding();
upcomingOffset = mPendingScrollPositionOffset - current;
}
if (upcomingOffset > 0) {
extraForStart += upcomingOffset;
} else {
extraForEnd -= upcomingOffset;
}
}
}
int startOffset;
int endOffset;
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL
: LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD
: LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL;
}
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// noRecycleSpace not needed: recycling doesn't happen in below's fill
// invocations because mScrollingOffset is set to SCROLLING_OFFSET_NaN
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
}
1、更新anchorInfo
anchorInfo 包含mPosition 开始填充child的位置。mCoordinate : 指定填充位置的偏移量。寻找anchorInfo有三种方式如下。
1第一种:updateAnchorFromPendingData(state, anchorInfo) 其中mPosition对应mPendingScrollPosition,mCoordinate对应
mPendingScrollPositionOffset 。这2个值都是scrollToPositionWithOffset(int, int)传递的。
private boolean updateAnchorFromPendingData(RecyclerView.State state, LinearLayoutManager.AnchorInfo anchorInfo) {
// 如果传入的偏移量无效
if (mPendingScrollPositionOffset == INVALID_OFFSET) {
View child = findViewByPosition(mPendingScrollPosition);
if (child != null) {
final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
if (childSize > mOrientationHelper.getTotalSpace()) {
// item does not fit. fix depending on layout direction
//从padding分配值
anchorInfo.assignCoordinateFromPadding();
return true;
}
final int startGap = mOrientationHelper.getDecoratedStart(child)
- mOrientationHelper.getStartAfterPadding();
//如果目标child的start小于padding,coordinate取值为padding,
//意思之后从padding处开始放置view,保证view能够看见。
if (startGap < 0) {
anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding();
anchorInfo.mLayoutFromEnd = false;
return true;
}
//如果目标child的end大于endafterpadding,coordinater就取endAfterpadding
//意思之后 从endAfterpadding开始放置View。 保证view能够看见
final int endGap = mOrientationHelper.getEndAfterPadding()
- mOrientationHelper.getDecoratedEnd(child);
if (endGap < 0) {
anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding();
anchorInfo.mLayoutFromEnd = true;
return true;
}
//可见,就从原来child的start开始防止view,等于保持不动。
anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd
? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper
.getTotalSpaceChange())
: mOrientationHelper.getDecoratedStart(child);
} else { // item is not visible.
if (getChildCount() > 0) {
// get position of any child, does not matter
int pos = getPosition(getChildAt(0));
anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos
== mShouldReverseLayout;
}
//找不到就从top(startpadding) 或者bottom(endafterpadding)开始布局。
anchorInfo.assignCoordinateFromPadding();
}
return true;
}
//如果mPendingScrollPositionOffset 不是无效,就从padding + 偏移量开始布局。
anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
// if this changes, we should update prepareForDrop as well
if (mShouldReverseLayout) {
anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding()
- mPendingScrollPositionOffset;
} else {
anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding()
+ mPendingScrollPositionOffset;
}
return true;
}
上述代码详细写了寻找position和mCoordinate过程,目的就是保证的开始位置和偏移量。调用scrollToPositionWithOffset(32, Int.MIN_VALUE)只会创建需要的view。
pendingAnchor.png
如上图所以,调用scrollToPositionWithOffset(32, itemheight)
anchorposition就等于32,而anchorcoordinate为itemheight,layout开始layoutend ,32->33->34->35->36->37 然后layoutstart ,31 ->30 如果mAvaiable>0 ,可以填充的空间大于0,再从lastelement处layoutend一次。
第二种 通过指定view 确定从哪儿开始放置View。
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
RecyclerView.State state, LinearLayoutManager.AnchorInfo anchorInfo) {
//找到一个view,如果这个不是remove, 没有超出边界,返回该View
//view的top就是Coordinate view的位置就是mPosition。
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
return true;
}
return false;
}
注意这个view是非remove的,view的top就是Coordinate ,view的位置就是mPosition。
第三种方式 也是默认的方式。通过padding安排position和偏移量。
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0
2 处理缓存
detachAndScrapAttachedViews(recycler);
这个方法就是清理屏幕上的holder,存到缓存策略中。比如notifydatasetchanged,所有的数据标记为无效,因此会存到pool中。而notifyItemchanged(index) index数据会被存放到changeScrap中,动画执行完成会存放到pool中,非index数据会被存放到attachScrap中,布局的时候直接从attachscrap中获取,onlayout完成后,会清空attachScrap中的holder。
3 填充LayoutState信息
1、mAvailable :child布局可用大小,默认都是recyclerview的大小。mAvailable = mOrientationHelper.getEndAfterPadding() - offset,offset默认为startPadding()
2、mItemDirection : adapter的数据方向。mShouldReverseLayout,manager的第三个参数。
3、mLayoutDirection : layout的布局方向。对于anchor的位置来说。LAYOUT_END 向下,LAYOUT_START 向上。
4、mCurrentPosition :当前要放置view的位置。
5、mOffset :每次layout下一个view的开始位置。
6、mScrollingOffset : 处于滚动状态的时候,代表一个滚动距离,我们不用创建新得view。预读取的时候用到,比如: 手指向上滚动,滚动距离大于最后一个view的不可见距离(end),代表我们要add新的view了,否则不需要创建。
4 填充layout
int fill(RecyclerView.Recycler recycler, LinearLayoutManager.LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
// 滑动时候,因为Mavailable = 滑动距离 - 不要创建view的距离。所以可能为负数。
// mAvailable = 5 - 12 = -7
if (layoutState.mAvailable < 0) {
//mScrollingOffset = 12 -7 = 5(滑动距离) 虽然end方向的view不需要创建,头(start)可能需要回收。
//因为所有item高度可能各不同。
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LinearLayoutManager.LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//measure和 layout
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
//对于remove的item在真正布局的时候 layoutChunkResult.mIgnoreConsumed 为true
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
// 滚动的时候回收item
if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
//如果可填充空间小于0 ,则 不要创建view的距离 也应该减少。
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
return start - layoutState.mAvailable;
}
1、 layoutChunk(recycler, state, layoutState, layoutChunkResult); 从缓存策略中加载一个view, addView(view)。
2、 measureChildWithMargins(view, 0, 0) 测量child,当width或者height支持滚动,则child的wrap_content,child的最终测量模式为MeasureSpec.makeMeasure(0,Measuspec.UNSPECIFIED),不支持滚动和viewgroup计算测量规则相同。 测量完成,child的left、right、top、bottom就能够确定了。 比如方向为LayoutState.LAYOUT_START也就是从anchor位置向上布局。layoutstate.mOffset就是每次布局start的位置,因此 top = layoutState.mOffset - result.mConsumed; bottom = layoutState.mOffset; 每一个item布局完成之后, 更新下次开始的位置layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = View.MeasureSpec.EXACTLY;
} else if (childDimension == RecyclerView.LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case View.MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = View.MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == RecyclerView.LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = View.MeasureSpec.UNSPECIFIED;
}
}
3、layoutDecoratedWithMargins(view, left, top, right, bottom); 此时的left,top等都是包含了mDecorInsets和child的margin了,因此真正layout的时候需要减去margin和decor。
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
5 scrollerBy回收holder
recycleByLayoutState(recycler, layoutState); 回收holder, 手指向上滚动,布局方向就是layout_END 此时回收的start. 手指向下滚动,回收的就是end位置的holder。
private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
final int childCount = getChildCount();
final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedStart(child) < limit
|| mOrientationHelper.getTransformedStartWithDecoration(child) < limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
}
当滚动的时候,layoutState.mScrollingOffset 代表多少距离,不用创建新holder, 如果大于这个距离,就需要创建holder,主要用于预读取时候,比如有2列的holder,用mScrollingoffset排序,值越小代表马上滚出去了,需要立刻创建holder。这里主要用于缓存holder,代表已经产生了多少滚动量。 以上面代码为例,回收底部end的holder时,如果getDecoratedStart(child)< limit 就代表childcount -1 到 i位置(不包括i)的holder 都会被滚出了屏幕。
从底部回收holder.png
如上图所示,item35的start 小于红线(limit) ,因此回收36和37位置holder。也可以采用item36、item37的start大于红线,每次回收一个,用小于红线,方便一次回收多个holder。
scrollBy
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
final int layoutDirection = delta > 0 ? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;
final int absDelta = Math.abs(delta);
updateLayoutState(layoutDirection, absDelta, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
mOrientationHelper.offsetChildren(-scrolled);
if (DEBUG) {
Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);
}
mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// calculate how much we can scroll without adding new children (independent of layout)
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
}
1、scrollingOffset 用最后一个child的end 减去getEndAfterPadding 就是最后一个view被遮挡的部分,也是滚动不需要创建view的大小。
2、mCurrentPosition 最后一个child的位置 + 加上layoutend的方向1。
3、mOffset 最后一个child的end位置,就是下一个view填充的位置。
4、comsumed等于不需要滚动的 + fill填充的空间 ,然后 调用offsetChildren(-scrolled)实现滚动。
GridLayoutManager
GridLayoutManager继承LinearLayoutManager,重写child的OnMeasure和onLayout方法。
1、计算anchorPosition
和LinearLayoutManager计算anchorinfo的三种方式一样,然后确保布局的anchorposition所在的spanIndex值为0,也就是保证从整行或者整列开始measure和layout。
private void ensureAnchorIsInCorrectSpan(RecyclerView.Recycler recycler,
RecyclerView.State state, AnchorInfo anchorInfo, int itemDirection) {
final boolean layingOutInPrimaryDirection =
itemDirection == LayoutState.ITEM_DIRECTION_TAIL;
int span = getSpanIndex(recycler, state, anchorInfo.mPosition);
if (layingOutInPrimaryDirection) {
// choose span 0
//只有span = 0 跳出循环,代表mPosition 是layout在新行的
while (span > 0 && anchorInfo.mPosition > 0) {
anchorInfo.mPosition--;
span = getSpanIndex(recycler, state, anchorInfo.mPosition);
}
} else {
// choose the max span we can get. hopefully last one
final int indexLimit = state.getItemCount() - 1;
int pos = anchorInfo.mPosition;
int bestSpan = span;
while (pos < indexLimit) {
//如果 postion+1 的index 小于 position 则表示positon +1
//换行了,next的值是一定为0的。 position + 1就是layout
//开始的位置
int next = getSpanIndex(recycler, state, pos + 1);
if (next > bestSpan) {
pos += 1;
bestSpan = next;
} else {
break;
}
}
anchorInfo.mPosition = pos;
}
}
getSpanIndex(recycler, state, anchorInfo.mPosition) 返回mPosition位置的index,index的值域为[0,spancount),如果为0,则mPostion应该把item放置的开始位置为新行的第0个位置。 如果为1,则mPostion的开始放置item位置就是当前行/列 1。
如上所示, 当span = 0的时候,就返回该位置。span=0 就代表mPosition位置为行/列的第0个位置。
2、计算每个item的大小
计算的是非滑动方向,保证每个itemsize * spancount = totalSize。 计算每一个item的大小存到数组中,如果非滑动方向measuremode为match_parent 则totalSpacesize为parentSize。如果是wrap_content,则totalSize = maxChildSize/spansize(item占的格数) * spancount。
static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
if (cachedBorders == null || cachedBorders.length != spanCount + 1
|| cachedBorders[cachedBorders.length - 1] != totalSpace) {
cachedBorders = new int[spanCount + 1];
}
cachedBorders[0] = 0;
int sizePerSpan = totalSpace / spanCount;
int sizePerSpanRemainder = totalSpace % spanCount;
int consumedPixels = 0;
int additionalSize = 0;
for (int i = 1; i <= spanCount; i++) {
int itemSize = sizePerSpan;
additionalSize += sizePerSpanRemainder;
if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
additionalSize -= spanCount;
}
consumedPixels += itemSize;
cachedBorders[i] = consumedPixels;
}
return cachedBorders;
}
举例:totalSpace = 38 ,spanCount = 5 结果cachedBorders[1]=8 , cachedBorders[2]=8+7 = 15 ,cachedBorders[3]=15+8=23, cachedBorders[4]=23 + 7 = 30,cachedBorders[5]= 30 + 8 = 38,由于38 / 5 = 7...3 余数为3,因此,这个算法保证 将剩余的3均分给item。
3、onMeasure和onLayout
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result) {
final int otherDirSpecMode = mOrientationHelper.getModeInOther();
final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
// if grid layout's dimensions are not specified, let the new row change the measurements
// This is not perfect since we not covering all rows but still solves an important case
// where they may have a header row which should be laid out according to children.
if (flexibleInOtherDir) {
updateMeasurements(); // reset measurements
}
final boolean layingOutInPrimaryDirection =
layoutState.mItemDirection == LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL;
int count = 0;
int consumedSpanCount = 0;
int remainingSpan = mSpanCount;
if (!layingOutInPrimaryDirection) {
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
remainingSpan = itemSpanIndex + itemSpanSize;
}
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
int pos = layoutState.mCurrentPosition;
final int spanSize = getSpanSize(recycler, state, pos);
if (spanSize > mSpanCount) {
throw new IllegalArgumentException("Item at position " + pos + " requires "
+ spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ " spans.");
}
remainingSpan -= spanSize;
if (remainingSpan < 0) {
break; // item did not fit into this row or column
}
View view = layoutState.next(recycler);
if (view == null) {
break;
}
consumedSpanCount += spanSize;
mSet[count] = view;
count++;
}
if (count == 0) {
result.mFinished = true;
return;
}
int maxSize = 0;
float maxSizeInOther = 0; // use a float to get size per span
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, layingOutInPrimaryDirection);
for (int i = 0; i < count; i++) {
View view = mSet[i];
if (layoutState.mScrapList == null) {
if (layingOutInPrimaryDirection) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (layingOutInPrimaryDirection) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
calculateItemDecorationsForChild(view, mDecorInsets);
measureChild(view, otherDirSpecMode, false);
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
final GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
/ lp.mSpanSize;
if (otherSize > maxSizeInOther) {
maxSizeInOther = otherSize;
}
}
if (flexibleInOtherDir) {
// re-distribute columns
guessMeasurement(maxSizeInOther, currentOtherDirSize);
// now we should re-measure any item that was match parent.
maxSize = 0;
for (int i = 0; i < count; i++) {
View view = mSet[i];
measureChild(view, View.MeasureSpec.EXACTLY, true);
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
}
}
// Views that did not measure the maxSize has to be re-measured
// We will stop doing this once we introduce Gravity in the GLM layout params
for (int i = 0; i < count; i++) {
final View view = mSet[i];
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
final GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) view.getLayoutParams();
final Rect decorInsets = lp.mDecorInsets;
final int verticalInsets = decorInsets.top + decorInsets.bottom
+ lp.topMargin + lp.bottomMargin;
final int horizontalInsets = decorInsets.left + decorInsets.right
+ lp.leftMargin + lp.rightMargin;
final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
final int wSpec;
final int hSpec;
if (mOrientation == VERTICAL) {
wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
horizontalInsets, lp.width, false);
hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
View.MeasureSpec.EXACTLY);
} else {
wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets,
View.MeasureSpec.EXACTLY);
hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
verticalInsets, lp.height, false);
}
measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
}
}
result.mConsumed = maxSize;
int left = 0, right = 0, top = 0, bottom = 0;
if (mOrientation == VERTICAL) {
if (layoutState.mLayoutDirection == LinearLayoutManager.LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = bottom - maxSize;
} else {
top = layoutState.mOffset;
bottom = top + maxSize;
}
} else {
if (layoutState.mLayoutDirection == LinearLayoutManager.LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = right - maxSize;
} else {
left = layoutState.mOffset;
right = left + maxSize;
}
}
for (int i = 0; i < count; i++) {
View view = mSet[i];
GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.hasFocusable();
}
Arrays.fill(mSet, null);
}
总结一下分为以下几步
1、 View view = layoutState.next(recycler); 从缓存策略中获取view,获取spancount个,存到View[]中。
2、 assignSpans(recycler, state, count, layingOutInPrimaryDirection); 通过position把需要的参数绑定到view的layoutparams上,方便后面使用。 spanSize : 占的格数。 spanindex : 位于span中的第几个。
3、 遍历数组,addView(view);
4、 遍历数组,calculateItemDecorationsForChild(view, mDecorInsets); 获取decorInset。
5、测量每一个view大小。measureChild(view, otherDirSpecMode, false)。非滚动的measureMode为otherDirMode,意思是采用viewgroup测量流程,也就是recyclerView和parent共同决定的mode。非滚动的大小为: getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize),也就是数组存的格子。
6、如果非滚动mode为wrap_content,再次测量。
measureChild(view, View.MeasureSpec.EXACTLY, true); 因为使用wrap_content测量的每一个item大小不一致。计算一个最大的itemMaxSize ,然后Math.min(itemMaxSize * spancount , oldDirSize) , 再次计算mCachedBorders数组。这次均分的数值就是最终的数值.。保证了当前行宽度一致,因此这次的mode为MeasureSpec.EXACTLY,再次记录高度的最大值。
7、因为宽高有wrap,则高度可能不确定,所以宽度不一致,6解决了宽度为wrap的情况,可能导致高也改变了。这里再次测量。
wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
horizontalInsets, lp.width, false);
hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
View.MeasureSpec.EXACTLY);
此时宽度,高度为定值。所以mode为View.MeasureSpec.EXACTLY
8、layout 因为每次测量一列/行。 每行高度一致。宽度存在mCachedBorders中。
left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
这里的right还可以写成 right = getpaddingleft() + mCachedBorders[params.mSpanIndex+ params.spanSize]
layoutDecoratedWithMargins(view, left, top, right, bottom) left 和top等都是包含了margin和decorinset的,因此child.layout需要减去。
总结
GridLayoutManager 和 LinearLayoutManager 承担着reyclerview的Measure和Layout的功能,两者的复用View逻辑相同,但是Grid的cacheScrap的最大个数为spancount(预读取) + 2 个, 而Linear的最大个数为 1(预读取) +2 个。 两者取anchor, 填充layoutState,scroller 逻辑相似。linear的measure是借助recyclerview测量,grid复写了measure ,grid每次会measure和layout spancount个view。
网友评论