Android实现RecyclerView的ItemDecoration

RecyclerView出来已经好久了,关于ItemDecoration的文章也一大堆,我也来讲讲ItemDecoration的使用方式.

源码

使用过ListView/GridView或多或少都有在每个Item中间添加间隔物这样的需求,也都提供了相应的添加方式.RecyclerView也不例外,只不过Google只是提供一个叫ItemDecoration的类,并没有提供比较好的实现类,所以这个时候就需要自己来实现了.

首先,创建一个类继承ItemDecoration,然后实现2个方法

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

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        
    }

由于父类的方法是空实现,所以也没必要调用super.

getItemOffsets:

这里要用到outRect这个参数,outRect这个参数的作用就是,设置当前view 左,上,右,下 的偏移量.

Android实现RecyclerView的ItemDecoration_第1张图片Android实现RecyclerView的ItemDecoration_第2张图片

onDraw:

如果玩过自定义View,就对onDraw这个方法非常熟悉.onDraw在这里的作用是在整个RecyclerView里面将所有j间隔物都绘制出来注意:这里是必须将所有的间隔物绘制出来.不像 getItemOffsets 方法一样,每add一个view的时候执行一次.所以在绘制的时候需要拿到所有的子view,然后根据每个子view的位置将间隔物绘制出来.所以在测试阶段,本人是比较建议为子view设置圆角背景,这样在计算间隔坐标的时候如果计算错误也比较容易看出来,因为这个时候就填充圆角,而如果计算没出错的话,是不会将圆角填充的.

常用的列表布局有ListView和GridView,在RecyclerView中可以通过RecyclerView.getLayoutManager()的方法来获取并判断当前的布局和orientation的值

例子:

  • left有偏移量:  

    private final int DIVIDER_SIZE = 20;

    private final Paint mPaint = new Paint();

    public RecyclerDivider() {
        mPaint.setAntiAlias(true);
        mPaint.setColor(0xffff0000);
        mPaint.setStyle(Paint.Style.FILL);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            if (((GridLayoutManager) layoutManager).getOrientation() == RecyclerView.VERTICAL) {
                getGridVerticalItemOffsets(outRect, view, parent);
            } else {
                getGridHorizontalItemOffsets(outRect, view, parent);
            }
        } else {
            if (((LinearLayoutManager) layoutManager).getOrientation() == LinearLayoutManager.VERTICAL) {
                getLinearVerticalItemOffsets(outRect, view, parent);
            } else {
                getLinearHorizontalItemOffsets(outRect, view, parent);
            }
        }
    }

    private void getLinearHorizontalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        outRect.left = DIVIDER_SIZE;
    }
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            if (((GridLayoutManager) layoutManager).getOrientation() == RecyclerView.VERTICAL) {
                drawGridVerticalDivider(c, parent);
            } else {
                drawGridHorizontalDivider(c, parent);
            }
        } else {
            if (((LinearLayoutManager) layoutManager).getOrientation() == LinearLayoutManager.VERTICAL) {
                drawLinearVerticalDivider(c, parent);
            } else {
                drawLinearHorizontalDivider(c, parent);
            }
        }

    }

    private void drawLinearHorizontalDivider(Canvas c, RecyclerView parent) {
        //获取有多少个子view
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            //拿到遍历到的子View
            final View child = parent.getChildAt(i);
            //获取child左边的位置并减去间隔物的宽度,得到间隔物的left
            final float left = child.getLeft() - DIVIDER_SIZE;
            final float top = child.getTop() - DIVIDER_SIZE;
            //由于是绘制在child的左边,所以间隔物的right等于child的left
            final float right = child.getLeft();
            final float bottom = child.getBottom();
            c.drawRect(left, top, right, bottom, mPaint);
        }
    }

现在讲的LinearLayoutManager.VERTICAL的情况,所以另外几个可以先不管.

如果还想在上面添加间隔物的话,只需在outRect.top设置值就行了,剩下的就是onDraw的工作的.

为了更好的表达left和top分别是2个Rect,我用了2种颜色表示

Android实现RecyclerView的ItemDecoration_第3张图片

 private void getLinearHorizontalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        outRect.left = DIVIDER_SIZE;
        outRect.top = DIVIDER_SIZE;
    }
private void drawLinearHorizontalDivider(Canvas c, RecyclerView parent) {
    //获取有多少个子view
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        //拿到遍历到的子View
        View child = parent.getChildAt(i);
        mPaint.setColor(0xffff0000);
        //获取child左边的位置并减去间隔物的宽度,得到间隔物的left
        float left1 = child.getLeft() - DIVIDER_SIZE;
        //注意:这个时候就不能单纯的getTop.而是必须再剪掉DIVIDER_SIZE,不然左上角就会有一个地方
        //没有绘制.当然了,这个也可以留给下面去绘制,都一样.
        float top1 = child.getTop() - DIVIDER_SIZE;
        //由于是绘制在child的左边,所以间隔物的right等于child的left
        float right1 = child.getLeft();
        float bottom1 = child.getBottom();
        c.drawRect(left1, top1, right1, bottom1, mPaint);
        mPaint.setColor(0xff00ff00);
        //如果上面也要的话,就必须在上面也绘制一个Rect
        float left2 = child.getLeft();
        float top2 = child.getTop() - DIVIDER_SIZE;
        float right2 = child.getRight();
        float bottom2 = child.getTop();
        c.drawRect(left2, top2, right2, bottom2, mPaint);
    }
}

再来看看一个绘制在2个View中间的例子

private void getLinearHorizontalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        int index = parent.getChildAdapterPosition(view);
        int childCount = parent.getAdapter().getItemCount();
        if (index != childCount - 1) {
            outRect.right = DIVIDER_SIZE;
        }
    }

先来举个反例

 private void drawLinearHorizontalDivider(Canvas c, RecyclerView parent) {
        //获取有多少个子view
        int childCount = parent.getChildCount();
        //因为最后一个不能draw,所以去掉最后一个
        for (int i = 0; i < childCount-1; i++) {
            View child = parent.getChildAt(i);
            float left = child.getRight();
            float top = child.getTop();
            float right = child.getRight() + DIVIDER_SIZE;
            float bottom = child.getBottom();
            c.drawRect(left,top,right,bottom,mPaint);
        }
}

先说明一下,我这里的child总共有10个.

这个时候就会发现,第5个和第6个中间没有间隔物.这是因为通过parent.getChildCount()并不是拿到所有的count,而是拿到显示在屏幕的count.而这个时候第5个就是最后一个,所以并不会将间隔物绘制出来.

解决方法:

private void drawLinearHorizontalDivider(Canvas c, RecyclerView parent) {
        //获取有多少个子view
        int childCount = parent.getChildCount();
        //通过adapter拿到真正的count
        int actualChildCount = parent.getAdapter().getItemCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(child);
            //判断当前position是否为最后一个
            if (position != actualChildCount - 1) {
                float left = child.getRight();
                float top = child.getTop();
                float right = child.getRight() + DIVIDER_SIZE;
                float bottom = child.getBottom();
                c.drawRect(left,top,right,bottom,mPaint);
            }
        }
    }

当然了,可能有人会想到,别管那么多,直接都设置right,反正最后一个又看不到.但如果为RecyclerView设置paddingRight的话,就原形毕露了.后面会讲当遇到这种情况要怎么解决,因为不是只有最后一个才会这样.

Android实现RecyclerView的ItemDecoration_第4张图片

LinearLayoutManager大概就这么多内容,当orientation为Vertical的时候实现思路也差不多这样,我就不做过多的说明了.接下来是讲的是GridLayoutManager的情况.


先写几个在GridManager用得比较多的方法

    //在child的左边绘制间隔物
    private void drawLeftDivider(final Canvas canvas, final View child) {
        float left = child.getLeft() - DIVIDER_SIZE;
        float top = child.getTop();
        float right = child.getLeft();
        float bottom = child.getBottom();
        canvas.drawRect(left,top,right,bottom,mPaint);
    }

    //在child的上面绘制间隔物
    private void drawTopDivider(Canvas canvas, View child) {
        float left = child.getLeft();
        float top = child.getTop() - DIVIDER_SIZE;
        float right = child.getRight();
        float bottom = child.getTop();
        canvas.drawRect(left, top, right, bottom, mPaint);
    }

    //在child的右边绘制间隔物
    private void drawRightDivider(Canvas canvas,View child){
        float left = child.getRight();
        float top = child.getTop();
        float right = child.getRight() + DIVIDER_SIZE;
        float bottom = child.getBottom();
        canvas.drawRect(left,top,right,bottom,mPaint);
    }

    //在child的下面绘制间隔物
    private void drawBottomDivider(Canvas canvas,View child){
        float left = child.getLeft();
        float top = child.getBottom();
        float right = child.getRight();
        float bottom = child.getBottom() + DIVIDER_SIZE;
        canvas.drawRect(left,top,right,bottom,mPaint);
    }

    //绘制一个长宽都为DIVIDER_SIZE的间隔物
    private void drawSquareDivider(Canvas canvas,float left,float top){
        float right = left + DIVIDER_SIZE;
        float bottom = top + DIVIDER_SIZE;
        canvas.drawRect(left,top,right,bottom,mPaint);
    }

首先来个简单一点的,left和top都有偏移量

Android实现RecyclerView的ItemDecoration_第5张图片

private void getGridVerticalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        outRect.left = DIVIDER_SIZE;
        outRect.top = DIVIDER_SIZE;
    } 
private void drawGridVerticalDivider(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        for(int i =0;i < childCount ;i++){
            final View child = parent.getChildAt(i);
            drawLeftDivider(c,child);
            drawTopDivider(c,child);
            drawSquareDivider(c,child.getLeft() - DIVIDER_SIZE,child.getTop() - DIVIDER_SIZE);
        }
    }

接下来来个只在中间绘制的例子,这个例子是重点,注意看

首先来看个反例,垂直的分隔符就先随便写吧,不是这里要讲的重点

    private void getGridVerticalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
        int position = parent.getChildAdapterPosition(view);
        if ((position + 1) % spanCount != 0) {//不是最后一列
            outRect.right = DIVIDER_SIZE;
        }
        outRect.bottom = DIVIDER_SIZE;
    }
    private void drawGridVerticalDivider(Canvas c, RecyclerView parent) {
        int childCount = parent.getChildCount();
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            if ((i + 1) % spanCount != 0) {
                drawRightDivider(c,child);
            }
            drawBottomDivider(c, child);
            //这里最后一列会会将间隔物绘制到view的外面,当有个padding的时候一下子就可以
            //看出来,不过还是那句话,这不是这里的重点
            drawSquareDivider(c, child.getRight(), child.getBottom());
        }
    }

会发现每个child的宽度不都一样,具体原因我没看过源码我也不清楚

只是根据我的推测,可能是当orientation为vertical的时候,水平的空间是有限的,所以每个child的left和right的和必须相等,才能保证每个child的宽度一样大

所以就想到这样的解决方案

Android实现RecyclerView的ItemDecoration_第6张图片

Android实现RecyclerView的ItemDecoration_第7张图片

可以看出,这样做了之后每个间隔物的宽度是一样的,child的宽度也一样

    private void getGridVerticalItemOffsets(Rect outRect, View view, RecyclerView parent) {
        int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
        int position = parent.getChildAdapterPosition(view);
        outRect.left = position % spanCount * DIVIDER_SIZE / 5;
        outRect.right = (spanCount - position % spanCount - 1) * DIVIDER_SIZE / 5;
        outRect.bottom = DIVIDER_SIZE;
    }

整体实现代码

private void getGridVerticalItemOffsets(Rect outRect, View view, RecyclerView parent) {
    int index = parent.getChildAdapterPosition(view);
    int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
    int childCount = parent.getAdapter().getItemCount();
    int lines;
    //计算行数,如果个数不能整出spanCount则结果+1
    if (childCount % spanCount == 0) {
        lines = childCount / spanCount;
    } else {
        lines = childCount / spanCount + 1;
    }
    outRect.left = (int) (index % spanCount * DIVIDER_SIZE/ spanCount);
    outRect.right = (int) ((spanCount - index % spanCount - 1) * DIVIDER_SIZE/ spanCount);
    //不是最后一行的话bottom必须有偏移量
    if (index / spanCount != lines - 1) {
        outRect.bottom =DIVIDER_SIZE;
    }
}
private void drawGridVerticalDivider(Canvas c, RecyclerView parent) {
    //当前显示的childCount
    int childCount = parent.getChildCount();
    int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
    //通过adapter拿到真正的childCount
    int actualChildCount = parent.getAdapter().getItemCount();
    //真正的行数
    int actualLines;
    if (actualChildCount % spanCount == 0) {
        actualLines = actualChildCount / spanCount;
    } else {
        actualLines = actualChildCount / spanCount + 1;
    }
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        int position = parent.getChildLayoutPosition(child);
        //当不是第一列的时候,drawLeft
        if (i % spanCount != 0) {
            drawLeftDivider(c, child);
        }
        //当不是在adapter的最后一行的时候,drawBottom
        if (position / spanCount != actualLines - 1) {
            drawBottomDivider(c, child);
        }
        //当不是第一列且不是adapter的最后一行时候,draw左下角的小方块
        if (position % spanCount != 0 && position / spanCount != actualLines - 1) {
            drawSquareDivider(c, child.getLeft(), child.getBottom());
        }
    }
}

在我提供的通用类中还有一个type叫做all,表示的是除了在中间绘制外,还在第一列的左边绘制,最后一列的右边绘制.所以同理,5个负责6个间隔物,每个负责6/5,所以第一个的left=5/5,right=1/5,最后一个的left=1/5,right=5/5,这里我就不贴代码了,思路都一样.


接下来讲一下当orientation为horizontal时在中间draw间隔物的实现方式

和vertical同样道理,当要在中间绘制间隔物的时候,在垂直方向需要使每个child负责同样大小的间隔物,否则就会出现每个child的高度不完全相同.

private void getGridHorizontalItemOffsets(Rect outRect, View view, RecyclerView parent) {
    int index = parent.getChildAdapterPosition(view);
    int spanCount = ((GridLayoutManager) (parent.getLayoutManager())).getSpanCount();
    int childCount = parent.getAdapter().getItemCount();
    int columns;
    if (childCount % spanCount == 0) {
        columns = childCount / spanCount;
    } else {
        columns = childCount / spanCount + 1;
    }
    if (index / spanCount != columns - 1) {
        outRect.right = DIVIDER_SIZE;
    }
    outRect.top = (int) ((float) (index % spanCount * DIVIDER_SIZE) / spanCount);
    outRect.bottom = (int) ((float) ((spanCount - index % spanCount - 1) * DIVIDER_SIZE) / spanCount);
}
private void drawGridHorizontalDivider(Canvas c, RecyclerView parent) {
    int childCount = parent.getChildCount();
    int spanCount = ((GridLayoutManager) parent.getLayoutManager()).getSpanCount();
    int actualChildCount = parent.getAdapter().getItemCount();
    int columns;
    if (actualChildCount % spanCount == 0) {
        columns = actualChildCount / spanCount;
    } else {
        columns = actualChildCount / spanCount + 1;
    }
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        int position = parent.getChildLayoutPosition(child);
        if (position / spanCount != columns - 1) {
            drawRightDivider(c, child);
        }
        if (i % spanCount != 0) {
            drawTopDivider(c, child);
        }
        if (position / spanCount != columns - 1 && position % spanCount != 0) {
            drawSquareDivider(c, child.getRight(), child.getTop() - DIVIDER_SIZE);
        }
    }
}

效果图

Android实现RecyclerView的ItemDecoration_第8张图片

到了这里基本实现就已经完成,要看具体实现可以看源码.不过还有一个问题要说一下,当为RecyclerView四周围设置padding后会发现间隔物绘制到padding那里了

Android实现RecyclerView的ItemDecoration_第9张图片

这里我是用Paint的遮罩模式解决的,在RecyclerView的padding处使用遮罩模式clear掉,放在onDraw统一管理,这样就用每种类型都写一遍

private Paint clipPaint = new Paint();
public RecyclerDivider() {
    clipPaint.setAntiAlias(true);
    clipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
    //当有设置padding的时候,在padding部分子view不会显示,但分隔符会显示,所以需要利用遮罩模式和图层剪掉
    int id = c.saveLayer(0f, 0f, parent.getWidth(), parent.getHeight(), null, Canvas.ALL_SAVE_FLAG);
    if (layoutManager instanceof GridLayoutManager) {
        if (((GridLayoutManager) parent.getLayoutManager()).getOrientation() == RecyclerView.VERTICAL) {
            drawGridVerticalDivider(c, parent);
        } else {
            drawGridHorizontalDivider(c, parent);
        }
    } else {
        if (((LinearLayoutManager) layoutManager).getOrientation() == LinearLayoutManager.HORIZONTAL) {
            drawLinearHorizontalDivider(c, parent);
        } else {
            drawLinearVerticalDivider(c, parent);
        }
    }
    //覆盖部分clear掉
    c.drawRect(0f, 0f, parent.getPaddingLeft(), parent.getHeight(), clipPaint);
    c.drawRect(parent.getPaddingLeft(), 0, parent.getWidth() - parent.getPaddingRight(), parent.getPaddingTop(), clipPaint);
    c.drawRect(parent.getWidth() - parent.getPaddingRight(), 0, parent.getWidth(), parent.getHeight(), clipPaint);
    c.drawRect(parent.getPaddingLeft(), parent.getHeight() - parent.getPaddingBottom(), parent.getWidth() - parent.getPaddingRight(), parent.getHeight(), clipPaint);
    c.restoreToCount(id);
}

Android实现RecyclerView的ItemDecoration_第10张图片

最后再提醒一下,绘制间隔物的笔的颜色最后设置一个低于0xff的透明度,这样如果某些地方重复绘制也可以马上看出来.

你可能感兴趣的:(android)