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 左,上,右,下 的偏移量.
onDraw:
如果玩过自定义View,就对onDraw这个方法非常熟悉.onDraw在这里的作用是在整个RecyclerView里面将所有j间隔物都绘制出来注意:这里是必须将所有的间隔物绘制出来.不像 getItemOffsets 方法一样,每add一个view的时候执行一次.所以在绘制的时候需要拿到所有的子view,然后根据每个子view的位置将间隔物绘制出来.所以在测试阶段,本人是比较建议为子view设置圆角背景,这样在计算间隔坐标的时候如果计算错误也比较容易看出来,因为这个时候就填充圆角,而如果计算没出错的话,是不会将圆角填充的.
常用的列表布局有ListView和GridView,在RecyclerView中可以通过RecyclerView.getLayoutManager()的方法来获取并判断当前的布局和orientation的值
例子:
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种颜色表示
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的话,就原形毕露了.后面会讲当遇到这种情况要怎么解决,因为不是只有最后一个才会这样.
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都有偏移量
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的宽度一样大
所以就想到这样的解决方案
可以看出,这样做了之后每个间隔物的宽度是一样的,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);
}
}
}
效果图
到了这里基本实现就已经完成,要看具体实现可以看源码.不过还有一个问题要说一下,当为RecyclerView四周围设置padding后会发现间隔物绘制到padding那里了
这里我是用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);
}
最后再提醒一下,绘制间隔物的笔的颜色最后设置一个低于0xff的透明度,这样如果某些地方重复绘制也可以马上看出来.