RecyclerView LinearLayoutManager GridLayoutManager 测量布局解析

在项目使用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 方向,从底部往上堆叠

  1. 朝向end 方向,从上往下堆叠
  2. 滚动以满足堆栈的要求,创建布局状态.

翻译过来大概要做啥有个印象,但具体怎样还得下看。因为主要分析布局测量所以现在省略很多代码。

可以看到,方法代码很长。很多检测状态代码。我们要看的是第二步骤,填充相关。到后面看到关键方法 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 引起的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容