在项目使用RecyclerView中,使用GridLayoutManager。本以为每个itemView 宽是一样的但实际上并没有,这让我需要好好查看分析源码一番
当前的布局情况
RecyclerView layout_width = MATCH_PARENT,layout_height = WRAP_CONTENT. 子view,layout_width = MATCH_PARENT,layout_height = WRAP_CONTENT. 在子view 的布局中TextView 宽高都是wrap_content 因为只有一行,所以高是确定的。ImageView 设置为固定宽高,所以总的来说子itemView 高是固定的。但因为textview 宽是WRAP_CONTENT 所以宽不确定,但是MATCH_PARENT 所以使用父view 也就是RecyclerView 传递的宽度。
出现的问题
使用GridLayoutManager spancount 按照以上布局本以为每个itemView 宽高一直,但实际上如果textView 需要设置的文字过长,会造成文字长的宽度大于文字短的itemView。
GridLayoutManager
因为不知道什么原因导致,如果要深入理解只有查看对itemView的被layout是 还有测量的宽高是怎么回事。
GridLayoutManager 继承 LinearLayout。可以说LayoutManger 在 RecyclerView 是很重要的一部分,布局测量都在这里。入口是onLayoutChildren,GridLayoutManager 直接交给父类LinearLayoutManager了。所以这时候分析LinearLayotuManager
LinearLayoutManager#onLayoutChildren
可以看到方法开头的注释中把需要做的步骤说清楚了分为4个步骤
1.通过检查子view 和其它变量找到锚点坐标和锚点item position
2.朝向start 方向,从底部往上堆叠
- 朝向end 方向,从上往下堆叠
- 滚动以满足堆栈的要求,创建布局状态.
翻译过来大概要做啥有个印象,但具体怎样还得下看。因为主要分析布局测量所以现在省略很多代码。
可以看到,方法代码很长。很多检测状态代码。我们要看的是第二步骤,填充相关。到后面看到关键方法 fill .好在另起了一个方法fill
fill 方法
fill 方法逻辑很清晰,通过一个while 循环给子view layout。主要通过LayoutChunk 方法。可以看到layoutChunk 方法在addView 之后调用了measureChildWithMargins(child,widthUsed,heightUsed)
这里widthUsed,heightUsed 都传递的0.在方法内部。可以看到为啥方法名跟ViewGroup 中的一样但还要重写一份,当然是有其它操作。
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
这里是很明显RecyclerView 增加的很重要的一步操作,通过注释和代码可以明白 widthUsed,heightUsed.对每个itemView 被占据的padding.查看getItemDecoratinsetsForChild(child)
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
到这里似乎很明显我们的addItemDecoration 添加的Decoration 是在哪里起作用了。通过insets 循环把ItemDecoration 的getItemOffsets 的值相加,成为总的child 的padding一部分并在测量中使用。
接下来继续看measureChildWidthMargins.在对 widthUsed等赋值后又对标准方法 getChildMeasureSpec 做了重写。同样可以知道这是对子view 测量的关键方法。
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
简化过后 getChildMeasureSpec(parentSize,parentMode,padding,childDimension,canScroll),这里获取到的宽高,widthSpec,heightSpec 直接用于child.measure(widthSpec,heightSpec)进行测量。
接下来看方法getChildMeasureSpec。这个方法可谓是自定义ViewGroup 很重要虽然代码不多逻辑清晰。很多android提供的ViewGroup都是用这个方法,包括measureChildren这些方法。里面的逻辑也很好的解释一些问题布局问题。比如父view WRAP_CONTENT 子view MATCH_PARENT 怎么测量?这些答案都在这里。当然不是所有的ViewGroup都是用这个策略去测量,比如RecyclerView 就重写了。但大差不差,要仔细了解测量的方式还得查看源码。每次都对这里理解不清,导致一时看懂了但是不久后还是不太记得清楚。常看常新!!!
大概记住三点 1.子View childDeminsion > 0 ,则是MeasureSPec.EXACTLY ,size = childDeminsion
2.父View 和子View 出现WRAP_CONTENT 得出的MeasureSpec 就是AT_MOST的。
measuredSize = size
3.如果是父View UNSPECIFIED 未指定UNSPECIFIED
则子View 也是
接下来伪代码
size = Math.min(parentSize - padding,0)
resultSize = xx?
resultMode = xx?
switch(parentMode) {
case EXACTLY :
if (childDeminsion > 0) {
resultSize = childDeminsion
resultMode = MeasureSpec.EXACTLY
} else if (childDeminsion == MATCH_PARENT) {
resultSize = size
resultMode = MeasureSpec.EXACTLY
} else if (childDeminsino == WRAP_CONTENT) {
resultSize = size
resultMode = MeasureSpec.AT_MOST
}
break;
case AT_MOST :
if (childDeminsion > 0) {
resultSize = size
resultMode = MeasureSpec.EXACTLY
} else if (childDeminsion == MATCH_PARENT) {
resultSize = size
resultMode = MeasureSpec.AT_MOST
} else if (childDeminsion == WRAP_CONTENT) {
resultSize = size
resultMode = MeasureSpec.AT_MOST
}
break;
case UNSPECIFIED :
if (childDemision > 0) {
resultSize = size
resultMode = EXACTLY
} else if (childDemision == MATCH_PARENT) {
resultSize = size
resultMode = UNSPECIFIED
}else if (childDeminsion == WRAP_CONTENT) {
resultSize= size
resultMode = UNSPECIFIED
}
break;
}
以上是ViewGroup 的getChildMeasureSpec.在RecyclerView中还是有不一样的。首先是判断了canScroll 看代码吧
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
switch (parentMode) {
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
resultSize = size;
resultMode = parentMode;
break;
case MeasureSpec.UNSPECIFIED:
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
break;
}
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = parentMode;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
resultMode = MeasureSpec.AT_MOST;
} else {
resultMode = MeasureSpec.UNSPECIFIED;
}
}
}
//noinspection WrongConstant
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
很清晰的可以看到不同。到这里测量就完了。接下来回到layoutChunk,接下来就是layout 了
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);
}
可以看到就是根据OrientionHelper获取的left,top,right,bottom.然后根据测量得到的insets 布局到指定位置。
对于问题的分析
在这里可以看到对问题的解决暂时还没有办法。先分析看看问题,因为这是LinearLayoutManager分析的。可以看到getChildMeasureSpec 父view MATCH_PARENT 子View Match_PARENT 得到的是 parentSize - padding 。所以不同点的分析还应在于orientionHelper的分析。可以看到赋值在setOriention。在构造方法中默认是VERTICAL
public static OrientationHelper createOrientationHelper(
RecyclerView.LayoutManager layoutManager, @RecyclerView.Orientation int orientation) {
switch (orientation) {
case HORIZONTAL:
return createHorizontalHelper(layoutManager);
case VERTICAL:
return createVerticalHelper(layoutManager);
}
throw new IllegalArgumentException("invalid orientation");
}
在layoutChunk 中调用OrientationHelper
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
赋值result.mConsumed
接下来
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
}
符合我们条件的left = getPaddingLeft,right = left + mOrientationHelper.getDecoratedMeasurementInOther(view).这才是真正的宽,所以结合上面创建的 vertical orientiationHelepr。查看是怎么算的width
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
所以看代码还是回到layoutManager中。看getDecoratedMeasuredWidth
public int getDecoratedMeasuredWidth(@NonNull View child) {
final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
return child.getMeasuredWidth() + insets.left + insets.right;
}
也很简单,注释中也说了,是childMeasuredWidth 加上itemDecoration insets。以上是LinearLayoutManager,所以暂时没问题,接下来我们看GridLayoutManager,原来是重写了 layoutChunk,那么就看是怎么做的。不看不知道一看吓一跳,代码量很多,可以理解毕竟相对LinearLayout多了好几个参数。不过大概步骤是相似的。看measureChild()
private void measureChild(View view, int otherDirParentSpecMode, boolean alreadyMeasured) {
final LayoutParams lp = (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 availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
final int wSpec;
final int hSpec;
if (mOrientation == VERTICAL) {
wSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
horizontalInsets, lp.width, false);
hSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
verticalInsets, lp.height, true);
} else {
hSpec = getChildMeasureSpec(availableSpaceInOther, otherDirParentSpecMode,
verticalInsets, lp.height, false);
wSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getWidthMode(),
horizontalInsets, lp.width, true);
}
measureChildWithDecorationsAndMargin(view, wSpec, hSpec, alreadyMeasured);
}
以上测量代码,关键在于
final int availableSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
涉及到计算spanRange
int getSpaceForSpanRange(int startSpan, int spanSize) {
if (mOrientation == VERTICAL && isLayoutRTL()) {
return mCachedBorders[mSpanCount - startSpan]
- mCachedBorders[mSpanCount - startSpan - spanSize];
} else {
return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan];
}
}
到这里有点明白了。搞半天限制itemView宽高的决定在于mCachedBOrderS那么看看它是怎么赋值的
private void calculateItemBorders(int totalSpace) {
mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
}
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;
}
查找了一番调用路线
calculateItemBorders -> guessMeasurement -> layoutChunk ,看到没,这里又到了layoutChunk的地方,同时也是measureChild的方法。他们的顺序肯定也是按照逻辑,measuredChild 依赖于 cachedBorders 所以要先 calculateItemBorders ,看代码也是
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);
这里省略了前后代码。通过对gussMeasurement 在layoutChunk 中传递参数的分析。
guessMeasurement(maxSizeInOther, currentOtherDirSize);
中的 maxSizeInOther,currentOtherDirSize 分析,可以得知 通过循环最终是要maxSizeInOther 在取值四最大的,就像让每个itemView 能够显示的下,同时在calculateItemBorders 可以看到cacheBorders 对每个span ,这里有点不好理解,我们只定义了spanCount.什么是每个span. 可以这样人认为第一列就是span = 0 ,一直到cachedBorders[spanCount].及对每个span 的Border 做了限制。这里叙述有点不清楚,一是源码比较长,二是我还没有很详细分析,只是解决问题为主。到这里我大概明白了,现在我的问题是跟span 的boader 有关。不一致,准确的说boader[0] 跟boader[spanCount] 不一致导致后面测量布局等出现的宽不一样。所以解决的地方应该是修改传递的ItemDecoration 的 getItemOffset() 对 left,top,right,bottom 进行修改。总的来说根据源码分析
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
/ lp.mSpanSize;
if (otherSize > maxSizeInOther) {
maxSizeInOther = otherSize;
}
特别是 orientationHelper.getDecoratedMeasurementInOther 获取的是child.width + recyclerView.leftMargin + recyclerView.rightMargin.几乎可以在现在我的场景中等于是屏幕宽度,并且 除以lp.mSpanSize.这就是相当于每个itemView 宽度是相等的。最终决定显示出来itemView宽不一样是因为测量中由ItemDecorateion引起的 withUsed,heightUsed 引起的。