美文网首页
RecyclerView LinearLayoutManager

RecyclerView LinearLayoutManager

作者: hewking | 来源:发表于2019-01-08 18:21 被阅读24次

    在项目使用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 引起的。

    相关文章

      网友评论

          本文标题:RecyclerView LinearLayoutManager

          本文链接:https://www.haomeiwen.com/subject/spmbrqtx.html