ItemDecoration用法

1、ItemDecoration概念

在使用RecyclerView显示列表的时候需要分隔线隔开item,此时则可以使用Recycler的内部类ItemDecoration。ItemDecoration是android系统提供的基类,用于绘制RecyclerView的分隔线,我们可以通过继承ItemDecoration实现丰富的分隔线效果。(系统也提供了实现好的DividerItemDecoration使用。)

2、ItemDecoration的使用

使用ItemDecoration时,需继承此类并覆写其中的getItemOffsets()和onDraw()方法即可。其中getItemOffsets()有两个重载的方法,如下:

public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)

public void getItemOffsets(@NonNull Rect outRect, int itemPosition, @NonNull RecyclerView parent)

其中第二个方法已经过时不推荐使用了。

3、getItemOffsets()

3.1、分析

android官网的说明如下:

Retrieve any offsets for the given item. Each field of outRect specifies the number of pixels that the item view should be inset by, similar to padding or margin.

该方法的outRect参数会与item进行inset运算,从而决定显示区型域。Rect的inset运算规则如下:

    /**
     * Insets the rectangle on all sides specified by the insets.
     * @hide
     * @param left The amount to add from the rectangle's left
     * @param top The amount to add from the rectangle's top
     * @param right The amount to subtract from the rectangle's right
     * @param bottom The amount to subtract from the rectangle's bottom
     */
    public void inset(int left, int top, int right, int bottom) {
        this.left += left;
        this.top += top;
        this.right -= right;
        this.bottom -= bottom;
    }

从上面代码可以看出系统会根据outRect的四个值来扩大item的区域,给分隔线留下绘制的空间。

If you need to access Adapter for additional data, you can call getChildAdapterPosition(View) to get the adapter position of the View.

如果需要获取item的数据自然顺序,可以调用recycler的getChildAdapterPosition(View)方法。

3.2、覆写

覆写该方法最核心的就是定义outRect的值,即确定分隔线的绘制区域。注意在绘制的时候需要区分是LinearLayoutManager还是GridLayoutManager。

如下代码是使用LinearLayout时添加分隔线。

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

    LinearLayoutManager lm = (LinearLayoutManager) parent.getLayoutManager();
    if (lm.getOrientation() == LinearLayoutManager.HORIZONTAL) {
        // 画垂直线
        outRect.set(0, 0, mDividerWidth, 0);   // 指在原有的item的基础上,将右边加10
    } else {
        // 画水平线
        outRect.set(0, 0, 0, mDividerWidth);   // 指在原有的item基础上,将底部加10
    }
}

在绘制每个item的时候系统都会回调该方法,在item相应的边加上分隔线的宽度。
在开发过程中有时不需要最后一行或是最后一列的分隔线,则可以通过逻辑判断来设置分隔线的区域。

如下判断是否是最后一行

private boolean isLastRow(GridLayoutManager layoutManager, RecyclerView parent, View view) {
    int spanCount = layoutManager.getSpanCount();       // 列数
    int itemCount = parent.getAdapter().getItemCount();
    int currentPosition = parent.getChildAdapterPosition(view);

    int lastRowCount = itemCount % spanCount == 0 ? spanCount : itemCount % spanCount;
    if ((currentPosition + lastRowCount) - itemCount >= 0) {
        return true;
    }
    return false;
}

如下判断是否是最后一列

private boolean isLastColumn(GridLayoutManager layoutManager, RecyclerView parent, View view) {
    int spanCount = layoutManager.getSpanCount();       // 现在无法关联源码,后面看一下,这个应该是列数
    int currentPosition = parent.getChildAdapterPosition(view);
    if ((currentPosition + 1) % spanCount == 0) {
        return true;
    }
    return false;
}

利用如上的方法可以将分隔线的绘制区域设置为0,则可以不显示分隔线。对于LinearLayoutManager则直接判断是否是最后一个元素即可。
但在实际的编码过程中发现,即使这样加入了处理存在如下问题。

在recyclerview的内容没有超过recyclerview的高度时(即没有发生滚动的情况下),即使不预留分割线的宽高也会绘制出分割线。

暂时还没搞清楚这个问题?

该问题可以通过确定最后一行元素的序号,在onDraw方法中不进行绘制避开。

4、onDraw方法分析

在确定了分隔线的绘制区域后,接下来就要根据区域绘制具体的内容了。
系统提供了两个绘制分隔线的方法。

public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)

该方法会在绘制item之前调用

public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)

该方法会在绘制item之后调用,
实际的效果二者差别不是很明显,一般选用第一个方法。

如下代码是绘制水平分割线(宽度与item相同),绘制的区域是根据getItemOffsets()确定的(在每个item的底边绘制),每个item都需要动态计算。

    private void drawHorizontal(@NonNull RecyclerView parent, Canvas canvas) {
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams rlp = (RecyclerView.LayoutParams) child.getLayoutParams();

            int left = child.getLeft() - rlp.leftMargin;
            int right = child.getRight();
            int top = child.getBottom() + rlp.bottomMargin;
            int bottom = top + mDividerWidth;      

            p.setColor(Color.GRAY);
            Rect r = new Rect(left, top, right, bottom);
            canvas.drawRect(r, p);
        }
    }

绘制竖直分割线(高度与item相同,在每个item的右边绘制),同上。

    private void drawVertical(@NonNull RecyclerView parent, Canvas canvas) {
        // 画垂直线
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams rlp = (RecyclerView.LayoutParams) child.getLayoutParams();

            int left = child.getRight() + rlp.rightMargin;
            int top = child.getTop() - rlp.topMargin;
            int right = left + 10;
            int bottom = child.getBottom() + rlp.bottomMargin;

            p.setColor(Color.RED);
            Rect r = new Rect(left, top, right, bottom);
            canvas.drawRect(r, p);
        }
    }

5、改进绘制区域

如果将分割线的宽度进行放大,那么很容易发现,分割线占用了item的宽度,不绘制最后一行带或最后一列会导致item的宽度大小不一致。
具体分析见此篇文章,分析的很全面。具体的做法如下:
先计算出每个item的偏移宽度,如下:

eachWidth =(spanCount-1)* dividerWidth / spanCount; // spanCount是列数,dividerWidth是分割线宽度。(将两个divider的宽度均分到3个item上)

在根据每一个item的right和下一个item的left相加等于dividerWidth。从而递推出每个item的left表达式

left = itemPosition % spanCount * (dividerWidth - eachWidth); // itemPosition是item的顺序

那么,right则可以如下表示

right = eachWidth - left;

故对于GridLayoutManager中item的宽度不一致的问题可以修正如下:

if (parent.getLayoutManager() instanceof GridLayoutManager) {
    GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
    int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
    int spanCount = layoutManager.getSpanCount();       // 列数

    boolean isLastRow = isLastRow(layoutManager, parent, view);

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

    int eachWidth = (spanCount - 1) * mDividerWidth / spanCount;

    left = itemPosition % spanCount * (mDividerWidth - eachWidth);
    right = eachWidth - left;
    bottom = mDividerWidth;
    if (isLastRow){
        bottom = 0;
    }
    outRect.set(left, top, right, bottom);
}

6、开源库及参考

  • item decoration
  • simple item decoration, 其中StartOffsetItemDecoration关于添加RecyclerView的偏移的思想可以学习一下。
  • 宽度不均问题参考
  • 启舰的这篇文章更好理解。

你可能感兴趣的:(Android,recyclerview,android)