RecyclerView是很强大的控件,基本可以替代ListView和GridView。但是RecyclerView没有封装一些listview的功能,例如分割线,item点击事件等等,需要自己实现。item点击事件在ViewHolder中设置Click事件就行了。实现分割线功能,则需要使用addItemDecoration添加一个自定义的分割线。
我使用的RecyclerView的版本是26.1.0,以下都是基于这个版本来讲的,先看一下ItemDecoration(item 装饰器)这个类的注释及方法。
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
*
* <p>All ItemDecorations are drawn in the order they were added, before the item
* views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
* and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
* RecyclerView.State)}.</p>
*/
public abstract static class ItemDecoration {
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn before the item views are drawn,
* and will thus appear underneath the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView
*/
public void onDraw(Canvas c, RecyclerView parent, State state) {
onDraw(c, parent);
}
/**
* @deprecated
* Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
/**
* Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
* Any content drawn by this method will be drawn after the item views are drawn
* and will thus appear over the views.
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
* @param state The current state of RecyclerView.
*/
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
onDrawOver(c, parent);
}
/**
* @deprecated
* Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
*/
@Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
/**
* @deprecated
* Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
*/
@Deprecated
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
outRect.set(0, 0, 0, 0);
}
/**
* Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
* The default implementation sets the bounds of outRect to 0 and returns.
*
* <p>
* If this ItemDecoration does not affect the positioning of item views, it should set
* all four fields of <code>outRect</code> (left, top, right, bottom) to zero
* before returning.
*
* <p>
* If you need to access Adapter for additional data, you can call
* {@link RecyclerView#getChildAdapterPosition(View)} to get the adapter position of the
* View.
*
* @param outRect Rect to receive the output.
* @param view The child view to decorate
* @param parent RecyclerView this ItemDecoration is decorating
* @param state The current state of RecyclerView.
*/
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
parent);
}
}
注释的大致意思:Item装饰器可以用来给item添加特殊的绘制图像和布局偏移(也就是padding)。可以实现分割线,高亮,视觉分组等等效果。3个方法的注释(3个过时方法对应3个不过时的方法不再讨论):
- onDraw:可以使用Canvas绘制各种装饰,在绘制视图之前触发,绘制的内容显示在视图下方(相当于背景)。
- onDrawOver:可以使用Canvas绘制各种装饰,在绘制视图之后触发,绘制的内容显示在视图上方。
- getItemOffsets:给outRect指定上下左右的边距大小,不指定默认为0,设置的大小就是item四周的padding。可以使用getChildAdapterPosition(View)来获取当前item的position。
关于onDraw,onDrawOver的使用可以参考hongyang大神的文章,这里主要讨论getItemOffsets设置边距。一般来说,设置分割线有以下几种,
- 左右相等,上下相等,每个item都设置的情况,不会导致item宽高不同,直接使用outRect.set(voffset, hoffset, voffset, hoffset)设置就可以了。
- 左右不同,上下不同,每个item都设置的情况,这时候也不会导致item的宽高不同,使用outRect.set(left, top, right, bottom)设置。
- 两边没有分割线中间有。这种情况常见于纵向的RecyclerView(GridLayoutManager,StaggeredGridLayoutManager),一般设置方式是:判断如果是第一列只设置右侧的间距,最后一列只设置左侧的间距,中间的设置左右两侧的间距。效果看上去是没问题的,但是实际上左右的两个item比较宽,宽了半个间距。
- 上下没有中间有,类似于3,上下两个item高了一个间距。
- 特殊的一种情况,左右或上下的分割线和中间的分割线相同。
先看正常的实现方式,代码如下
/**
* 普通设置间距(纵向)
*/
public class NormalItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount; // 每行个数
private int decoration; // 间距
private boolean includeEdge; // 是否需要左右,上下分割线
public NormalItemDecoration(int spanCount, int decoration, boolean includeEdge) {
this.spanCount = spanCount;
this.decoration = decoration;
this.includeEdge = includeEdge;
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int column = position % spanCount;// 计算这个child 处于第几列
if (column % spanCount == 0) {
// 第一列
if (includeEdge) {
outRect.left = decoration;
} else {
outRect.left = 0;
}
outRect.right = decoration / 2;
} else if ((column + 1) % spanCount == 0) {
// 最后一列
outRect.left = decoration / 2;
if (includeEdge) {
outRect.right = decoration;
} else {
outRect.right = 0;
}
} else {
outRect.left = decoration / 2;
outRect.right = decoration / 2;
}
if (includeEdge) {
if (position < spanCount) { // 第一行设置top
outRect.top = decoration;
}
outRect.bottom = decoration; // 设置bottom
} else {
if (position >= spanCount) {
outRect.top = decoration; // 非第一行设置top
}
}
}
}
看一下效果:无四周分割线时,左右的item会比较大,大了半个间隔。
有四周分割线时,左右的item比较小,小了一半的间隔。
当间距比较小时,肉眼看不出来,但是设置的间距比较大时就比较明显了。导致这个的原因就是间距就相当于padding,设置的padding不同,item的宽高就会不同。那么怎么解决呢,StackOverflow有这个解决方案,链接https://stackoverflow.com/a/30701422/4696538。以GridLayoutManager为例:间距设置为5dp,每行有4列,左右没有分割线,中间有分割线。第一列:左侧为0,右侧为4dp;第二列:左侧为1dp,右侧为3dp;第三列:左侧为2dp,右侧为2dp;第三列:左侧为3dp,右侧为1dp;第4列:左侧为4dp,右侧为0;这样就是分割线为5dp,而且宽度都相同。虽然如此,按照StackOverflow的解决方案来写的话也是会有一点儿小问题,因为间隔除以spanCount如果不是整数,可能导致item相差一两个像素。如下图
当然一两个像素,问题不算大,但是也可以解决,先计算出每个总的左右间距,每次只计算左边间距,右边间距则用总间距减去左侧获得,代码如下:
/**
* 校准过的设置间距(纵向)
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount; // 每行个数
private int decoration; // 间距
private boolean includeEdge; // 是否需要左右,上下分割线
private int total; // item总共的间隔
private int[] left; // left 数组
private int[] right; // right 数组
public GridItemDecoration(int spanCount, int decoration, boolean includeEdge) {
this.spanCount = spanCount;
this.decoration = decoration;
this.includeEdge = includeEdge;
if (includeEdge) {
total = decoration + Math.round(decoration * 1f / spanCount);
} else {
total = Math.round(decoration * (spanCount - 1) * 1f / spanCount);
}
left = new int[spanCount];
right = new int[spanCount];
for (int i = 0; i < spanCount; i++) {
if (i == 0) {
if (includeEdge) {
left[i] = decoration;
} else {
left[i] = 0;
}
} else {
left[i] = decoration - (total - left[i - 1]); // 后一列的left = decoration - (total - 上一列的left)
}
right[i] = total - left[i];
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,
@NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int column = position % spanCount;// 计算这个child 处于第几列
outRect.left = left[column];
outRect.right = right[column];
if (includeEdge) {
if (position < spanCount) { // 第一行设置top
outRect.top = decoration;
}
outRect.bottom = decoration; // 设置bottom
} else {
if (position >= spanCount) {
outRect.top = decoration; // 非第一行设置top
}
}
}
}
就是以加减代替除法,消除可能的精度问题。
效果如下
最后:纵向时top和bottom不会影响控件的高度。同理横向时就需要动态设置top和bottom。