RecyclerView GridLayoutManager item设置万能分隔线

本来没准备写Android的,在写SoapUI,做项目的时候,遇到了RecyclerView,并且使用GridLayoutManager样式,需要分隔:首尾两列有一定限制的间隔,行间距,列间距平均,可是在网上找了下,都不能达到目的,有几篇文章说到了点子上,但是也不能解决列与列之间平均间隔的问题。

所以就只能自己撸一撸了
GridDividerItemDecoration上传到GitHub:https://github.com/haozi5460/GridDividerItemDecoration

图1.1

图1.1 是我们UI锅需要的效果:

1、首尾两列与父布局间隔20dp

2、item 宽高固定为85dp

3、列与列之间间隔平均

4、行与行之间间隔20dp

百度了下,看到解决RecyclerView GridLayoutManager 设置分割线出现item宽度不等的问题这个文章,它能解决的是如果没有第一项,即首尾两列与父布局没有间隔,它能很好的布局。但是一旦需要首尾两列与父布局有间隔的话,它就不好使了,大家可以试一试。

下图是我撸完之后的效果:

图1.2
图1.3
图1.4

上面图1.2、1.3、1.4就是项目中时间效果图,不一定非得3个、4个 、5个也是能达到同样效果的。

这里肯定有同学问了,图上的UE工具是怎么回事,这里给大家安利一个好工具:饿了爸出品UETool工具。使用方法就自己去see一see了。

接下来,到了show me the code环节了。

一、首先我们得分析下RecyclerView源码,看看怎么测量子控件宽高的(方法为measureChild)

这样我们才能知道怎么去设置item的ourRect,前后左右的间距

/**
         * Measure a child view using standard measurement policy, taking the padding
         * of the parent RecyclerView and any added item decorations into account.
         *
         * <p>If the RecyclerView can be scrolled in either dimension the caller may
         * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
         *
         * @param child Child view to measure
         * @param widthUsed Width in pixels currently consumed by other views, if relevant
         * @param heightUsed Height in pixels currently consumed by other views, if relevant
         */
        public void measureChild(View child, int widthUsed, int heightUsed) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            widthUsed += insets.left + insets.right;
            heightUsed += insets.top + insets.bottom;
            final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                    canScrollHorizontally());
            final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                    canScrollVertically());
            if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
                child.measure(widthSpec, heightSpec);
            }
        }

这里就先只看宽度,也就是widthSpec变量,该值通过getChildMeasureSpec方法得到,进入该方法,代码如下:

/**
         * Calculate a MeasureSpec value for measuring a child view in one dimension.
         *
         * @param parentSize Size of the parent view where the child will be placed
         * @param parentMode The measurement spec mode of the parent
         * @param padding Total space currently consumed by other elements of parent
         * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
         *                       Generally obtained from the child view's LayoutParams
         * @param canScroll true if the parent RecyclerView can scroll in this dimension
         *
         * @return a MeasureSpec value for the child view
         */
        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);
        }

发现所需要的宽度也就是size变量,而size = Math.max(0, parentSize - padding),通过传进来的参数,可以得到size=getWidth()-(getPaddingLeft() + getPaddingRight() + widthUsed),说明子控件的宽度等于RecyclerView的宽度减去RecyclerView自身左右的padding值外,还要减去子控件的左右偏移量!!!
即我们想要保证item大小一致,列与列间隔一致,我们需要平均分配每个item的paddingLeft+paddingRight即左右偏移量都相等!

因为如果我们像下列来确定item左右间隔的话就会出现item大小不一,列与列之间间隔不一的情况:

    @Override
    public void getItemOffsets(Rect outRect, int itemPosition,
                               RecyclerView parent) {
        int spanCount = getSpanCount(parent);
        int childCount = parent.getAdapter().getItemCount();
        if (isLastRaw(parent, itemPosition, spanCount, childCount))// 如果是最后一行,则不需要绘制底部
        {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        } else if (isLastColum(parent, itemPosition, spanCount, childCount))// 如果是最后一列,则不需要绘制右边
        {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(),
                    mDivider.getIntrinsicHeight());
        }
    }

按上面分析的源码,我们可以知道,调用outRect.set(int left, int top, int right, int bottom)方法时,left一直为0,right一直为divider的宽度,而每一项item的宽度都要减去(left+right)大小,我这里设divider宽度为20,因此第一项item的宽度要减,20,第二项item宽度要减20,第三项item的left为0,right也为0,宽度要减0。因此第三项就比前面两项都宽20了,item宽度就明显不相同了。效果如下图:


图1.5

二、原理清楚了,我们该打通任督二脉了~

我们主要要对列与列之间的间距进行计算,行间距倒是很简单。

1、首先,我们并不固定输入列间距,我们通过对item view的宽度计算,如果item 宽高并没有指定,便自动分配,通过 屏幕宽度(widthPixels) - item宽度 * 列数(spanCount),我们就可以得到最大剩余的宽度值maxDividerWidth。
    /**
     * 获取Item View的大小,若无则自动分配空间
     * 并根据 屏幕宽度-View的宽度*spanCount 得到屏幕剩余空间
     * @param view
     * @return
     */
    private int getMaxDividerWidth(View view) {
        int itemWidth = view.getLayoutParams().width;
        int itemHeight = view.getLayoutParams().height;

        int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels > mContext.getResources().getDisplayMetrics().heightPixels
                ? mContext.getResources().getDisplayMetrics().heightPixels : mContext.getResources().getDisplayMetrics().widthPixels;

        int maxDividerWidth = screenWidth-itemWidth * spanCount;
        if(itemHeight < 0 || itemWidth < 0 || (isNeedSpace && maxDividerWidth <= (spanCount-1) * mDividerWidth)){
            view.getLayoutParams().width = getAttachCloumnWidth();
            view.getLayoutParams().height = getAttachCloumnWidth();

            maxDividerWidth = screenWidth-view.getLayoutParams().width * spanCount;
        }
        return maxDividerWidth;
    }
2、其次,获得最大剩余宽度后,我们要得出每个Item 的平均所占间隔eachItemWidth(即paddingLeft+paddingRight),和列与列之间我们应该设置的间隔大小dividerItemWidth
        int maxAllDividerWidth = getMaxDividerWidth(view); //获得最大剩余宽度

        int spaceWidth = 0;//首尾两列与父布局之间的间隔
        if(isNeedSpace)//判读首尾与父布局需要间隔
            spaceWidth = mDividerWidth;

        int eachItemWidth = maxAllDividerWidth/spanCount;//每个Item left+right
        int dividerItemWidth = (maxAllDividerWidth-2*spaceWidth)/(spanCount-1);//item与item之间的距离

每个Item 的平均所占间隔eachItemWidth:
我们通过 最大剩余宽度(maxAllDividerWidth) / 列数(spanCount) 得到

列与列之间我们应该设置的间隔大小dividerItemWidth:
在计算之前我们有个spaceWidth,这个变量用于保存首尾列与父布局的间隔值
dividerItemWidth = 最大剩余宽度(maxAllDividerWidth) 减去首尾与父布局的间隔在 除以 spanCount-1 (比如说3个item,列与列之间只有两个间隔)。

3、最后,在计算出以上变量值之后,我们需要对item的outRect left、top、bottom、right进行设置。

在设置之前,我们要分析下每个item的位移量,以便分析得出统一格式:
假设我们有3列,在这里我们只分析左右位移量,left、right
这里首尾与父布局间隔设为sW,每个Item 的平均所占间隔为eW,列与列之间设置的间隔为dW。简单的画个图理解一下:


图1.6

第一个Item:L0=sW                                  R0=eW-sW
第二个Item:L1=dW-R0=dW-eW+sW       R1=eW-L1=2eW-dW-sW
第三个Item:L2=dW-R1=2(dW-eW)+sW   R2=eW-L2=3eW-2dW-sW

所以根据以上可以得出
Ln = (position % spanCount) * (dW-eW) + sW
Rn = eW-Ln

left = itemPosition % spanCount * (dividerItemWidth - eachItemWidth) + spaceWidth;
right = eachItemWidth - left;

然后再根据你的需求设置行间距;第一行与父布局是否要间隔;最后一行是否有间隔等

        left = itemPosition % spanCount * (dividerItemWidth - eachItemWidth) + spaceWidth;
        right = eachItemWidth - left;
        bottom = mDividerWidth;
        if(mFirstRowTopMargin > 0 && isFirstRow(parent, itemPosition, spanCount, childCount))//第一行顶部是否需要间隔
            top = mFirstRowTopMargin;
        if (!isLastRowNeedSpace && isLastRow(parent, itemPosition, spanCount, childCount)){//最后一行是否需要间隔
            bottom = 0;
        }

        outRect.set(left, top, right, bottom);

以上是精华部分,下面附上getItemOffsets这个方法的全部代码

 @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);

        int top = 0;
        int left = 0;
        int right = 0;
        int bottom = 0;

        int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
        spanCount = getSpanCount(parent);
        int childCount = parent.getAdapter().getItemCount();
        int maxAllDividerWidth = getMaxDividerWidth(view); //获得最大剩余宽度

        int spaceWidth = 0;//首尾两列与父布局之间的间隔
        if(isNeedSpace)
            spaceWidth = mDividerWidth;

        int eachItemWidth = maxAllDividerWidth/spanCount;//每个Item left+right
        int dividerItemWidth = (maxAllDividerWidth-2*spaceWidth)/(spanCount-1);//item与item之间的距离

        left = itemPosition % spanCount * (dividerItemWidth - eachItemWidth) + spaceWidth;
        right = eachItemWidth - left;
        bottom = mDividerWidth;
        if(mFirstRowTopMargin > 0 && isFirstRow(parent, itemPosition, spanCount, childCount))//第一行顶部是否需要间隔
            top = mFirstRowTopMargin;
        if (!isLastRowNeedSpace && isLastRow(parent, itemPosition, spanCount, childCount)){//最后一行是否需要间隔
            bottom = 0;
        }

        outRect.set(left, top, right, bottom);
    }

使用这个GridDividerItemDecoration这个类用来分配Grid间距时,可以为item设置具体宽高,也可以设置为wrap_content,会自动分配,而分配的行间距、列间距都会平均。

我已将GridDividerItemDecoration,上传到GitHub:https://github.com/haozi5460/GridDividerItemDecoration

后期又撸了下一篇《不嵌套RecyclerView!!!实现有HeaderView的GridLayoutManager》,用于用一个RecyclerView(不嵌套RecyclerView) 实现下图效果,有兴趣或者有需要可以看看~

图1.7

申明:禁用于商业用途,如若转载,请附带原文链接。https://www.jianshu.com/p/fb7e1a0749d6 蟹蟹(#.#)