android FlexboxLayout采坑记
- google提供的弹性布局的方案:https://github.com/google/flexbox-layout
问题
子view被错误摆放位置
- 我在RecyclerView中使用了FlexboxLayout这个控件

- 上图是我使用FlexboxLayout后的预期摆放,第三行如果放到第二行末尾,会使总宽度超出父view所能容纳的最大宽度,因此这样摆放才是合理的,放不下就另起一行,这也就是Flexbox的作用
- 但是我一运行后,却发现是这样的

- 第三行被强行放到第二行,同时由于宽度收到父view的限制,因此第二行每个view的宽度都减少了一部分,导致原本一行能放得下的文本放不下,只能撑开高度
- 这显然是一个bug
- 那么就来看看为何子view会被错误地挤在一起
- 首先得理解下google工程师是怎么弹性布局的
- google会遍历每一个子view,然后根据情况将每个子view放在适合的线上,如下图,红色的箭头就表示那条线

- 很显然会出现第二种情况,就是因为google错误地将第二和第三个子view都放置到了第二条线上,也就是这里的计算出现了失误
- FlexboxLayout的计算主要都放在FlexboxHelper里
- com.google.android.flexbox.FlexboxHelper#isWrapRequired这个方法就是来决定子view是否可以放在同一行,还是另起一行的,如果不能放在同一行,那么就新增FlexLine,如果可以在同一行,那么就往这一行的FlexLine的个数加1
- 现在发现是第二个FilexLine的个数为2,说明flexboxlayout认为第二行能放下两个子view,但经过debug我发现两个子view的宽度加起来刚好等于父view能承受的最大宽度,由于两个子view之间还有间隔宽度,因此加上这个间隔宽度后两个子view是不可能放在一行的,因此已经超过了父view的最大值,应该另起一行才对
- 后面跟踪发现是在计算间隔时返回了0,代码在com.google.android.flexbox.FlexboxHelper#isWrapRequired方法里
private boolean isWrapRequired(View view, int mode, int maxSize, int currentLength,
int childLength, FlexItem flexItem, int index, int indexInFlexLine, int flexLinesSize) {
if (mFlexContainer.getFlexWrap() == FlexWrap.NOWRAP) {
return false;
}
if (flexItem.isWrapBefore()) {
return true;
}
if (mode == View.MeasureSpec.UNSPECIFIED) {
return false;
}
int maxLine = mFlexContainer.getMaxLine();
if (maxLine != NOT_SET && maxLine <= flexLinesSize + 1) {
return false;
}
int decorationLength =
mFlexContainer.getDecorationLengthMainAxis(view, index, indexInFlexLine);
if (decorationLength > 0) {
childLength += decorationLength;
}
return maxSize < currentLength + childLength;
}
int decorationLength =
mFlexContainer.getDecorationLengthMainAxis(view, index, indexInFlexLine);
- 计算出decorationLength 结果为0,此时参数里view为第三个子view,index为2,indexInFlexLine为0
- 继续追踪,发现是这里com.google.android.flexbox.FlexboxLayout#allViewsAreGoneBefore返回了true
private boolean allViewsAreGoneBefore(int index, int indexInFlexLine) {
for (int i = 1; i <= indexInFlexLine; i++) {
View view = getReorderedChildAt(index - i);
if (view != null && view.getVisibility() != View.GONE) {
return false;
}
}
return true;
}
- 当时我的条件是index为2,indexInFlexLine为0,因此返回了true,仔细想了下,此时的indexInFlexLine应该为1才对,怎么传了0?
- 于是得看看indexInFlexLine的源头是谁设置的,也就是isWrapRequired方法的参数是从哪设置的
- 找到代码如下
int indexInFlexLine = 0;
...
int childCount = mFlexContainer.getFlexItemCount();
for (int i = fromIndex; i < childCount; i++) {
...
if (isWrapRequired(child, mainMode, mainSize, flexLine.mMainSize,
getViewMeasuredSizeMain(child, isMainHorizontal)
+ getFlexItemMarginStartMain(flexItem, isMainHorizontal) +
getFlexItemMarginEndMain(flexItem, isMainHorizontal),
flexItem, i, indexInFlexLine, flexLines.size())) {
...
flexLine = new FlexLine();
flexLine.mItemCount = 1;
flexLine.mMainSize = mainPaddingStart + mainPaddingEnd;
flexLine.mFirstIndex = i;
indexInFlexLine = 0;
largestSizeInCross = Integer.MIN_VALUE;
} else {
flexLine.mItemCount++;
indexInFlexLine++;
}
- 从这段逻辑我们可以看到,即使子view被放在第二个位置,此时isWrapRequired的参数indexInFlexLine还是0,按照我们理解应该为1,如果子view被放在第三个位置,此时indexInFlexLine应该为2,即这里的合理的值应该为flexLine.mItemCount才对
- 因此只要修改isWrapRequired的第八个参数为flexLine.mItemCount即可,如下
if (isWrapRequired(child, mainMode, mainSize, flexLine.mMainSize,
getViewMeasuredSizeMain(child, isMainHorizontal)
+ getFlexItemMarginStartMain(flexItem, isMainHorizontal) +
getFlexItemMarginEndMain(flexItem, isMainHorizontal),
flexItem, i, flexLine.mItemCount, flexLines.size())) {
}
- 进过验证,确实得到了我的预期效果
- 但是问题又来了,我难道要把Flexbox这个库下载下来修改源码,再替换我工程里的引用吗
- 这样做就变成我要维护flexbox这个库了,后面如果有升级咋办,这显然是不行的
- 那么就只能通过修改字节码的形式去修复这个bug
- 具体该怎么做,待写…
- 另外如何快速引用修改后的源码来验证,待写…(感觉自己在挖坑)