在使用RecyclerView显示列表的时候需要分隔线隔开item,此时则可以使用Recycler的内部类ItemDecoration。ItemDecoration是android系统提供的基类,用于绘制RecyclerView的分隔线,我们可以通过继承ItemDecoration实现丰富的分隔线效果。(系统也提供了实现好的DividerItemDecoration使用。)
使用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)
其中第二个方法已经过时不推荐使用了。
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)方法。
覆写该方法最核心的就是定义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方法中不进行绘制避开。
在确定了分隔线的绘制区域后,接下来就要根据区域绘制具体的内容了。
系统提供了两个绘制分隔线的方法。
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);
}
}
如果将分割线的宽度进行放大,那么很容易发现,分割线占用了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);
}