recyclerview瀑布流_ByRecyclerView:真万能分割线 (线性/宫格/瀑布流)

通过ByRecyclerView库解决RecyclerView线性、宫格、瀑布流布局的自定义分割线问题,支持设置不同方向的间隔、显示数量,兼容横向和纵向布局。详细介绍了绘制原理及参数配置。
摘要由CSDN通过智能技术生成

cc3a740a10f934fdc70bc5bfc97d8c5e.png

作者:Jinbeen 链接:https://www.jianshu.com/p/dbb81c829e53


前言

我基本上找遍了网上所有通过ItemDecoration设置分隔线的文章,但都不尽如意,它们大多只适用于部分情况,比如只能给线性布局设置、只能设置color不能设置drawable、不能去除HeaderView部分的分割线、配置麻烦等等等。

于是我费尽周折出了两个类:SpacesItemDecorationGridSpaceItemDecoration。它们基本解决了上述所有问题!

收录于开源项目:ByRecyclerView

它们有什么功能

SpacesItemDecoration

LinearLayoutManager设置

  • 1、可设置colordrawable
  • 2、可设置分割线左右或上下的间距
  • 3、可设置headerfooter不显示分割线的个数,功能似ListViewsetHeaderDividersEnabled(ture)
  • 4、支持横向或纵向

GridSpaceItemDecoration

GridLayoutManagerStaggeredGridLayoutManager设置

  • 1、可配置只在四周是否显示分割线
  • 2、可设置headerfooter不显示分割线的个数
绘制原理

网上很多解释通过ItemDecoration绘制分割线的原理的文章,我简单总结一下,在getItemOffsets()方法里设置item宽度的偏移量,在onDraw()方法里主要绘制分割线颜色。getItemOffsets 是针对每一个 ItemView,而 onDraw 方法却是针对 RecyclerView 本身,所以在 onDraw 方法中需要遍历屏幕上可见的 ItemView,分别获取它们的位置信息,然后分别的绘制对应的分割线。-- 参考:https://juejin.im/post/5cecef7d5188250b3a1b9173

示例图

650543635f6b8834eef7a167c9abf6e2.png

f060964f192a8d2d4eeac1bbf20c7b60.png

参数配置

SpacesItemDecoration构造方法有四个:

SpacesItemDecoration(Context context)
SpacesItemDecoration(Context context, int orientation)
SpacesItemDecoration(Context context, int orientation, int headerNoShowSize)
/**
* @param context Current context, it will be used to access resources.
* @param orientation 水平方向or垂直方向,默认SpacesItemDecoration.VERTICAL
* @param headerNoShowSize 不显示分割线的item个数 这里应该包含刷新头
* @param footerNoShowSize 尾部 不显示分割线的item个数 默认不显示最后一个item的分割线
*/
public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize, int footerNoShowSize)

其他参数设置,其中setDrawable与setParam只能选择其一:

/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public SpacesItemDecoration setDrawable(Drawable drawable)/**
* 直接设置分割线颜色等,不设置drawable
*
* @param dividerColor 分割线颜色
* @param dividerSpacing 分割线间距
* @param leftTopPaddingDp 如果是横向 - 左边距
* 如果是纵向 - 上边距
* @param rightBottomPaddingDp 如果是横向 - 右边距
* 如果是纵向 - 下边距
*/public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing, float leftTopPaddingDp, float rightBottomPaddingDp)

一个完整的设置如下:

// 设置分割线color
SpacesItemDecoration itemDecoration = new SpacesItemDecoration(recyclerView.getContext(), SpacesItemDecoration.VERTICAL, 0, 1)
.setParam(R.color.colorLine, 1, 12, 12);
recyclerView.addItemDecoration(itemDecoration);

// 设置分割线drawable
SpacesItemDecoration itemDecoration = new SpacesItemDecoration(recyclerView.getContext(), SpacesItemDecoration.VERTICAL, 0, 1)
.setDrawable(R.drawable.shape_line);
recyclerView.addItemDecoration(itemDecoration);
核心代码

这里主要解释这几个参数配置的核心代码,具体请直接见源代码:

for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int childRealPosition = parent.getChildAdapterPosition(child);

// 过滤到头部不显示的分割线
if (childRealPosition < mHeaderNoShowSize) {
continue;
}
// 过滤到尾部不显示的分割线
if (childRealPosition <= lastPosition - mFooterNoShowSize) {

// 设置drawable
if (mDivider != null) {
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}

// 设置color
if (mPaint != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
// 首尾间距
int left1 = left + mLeftTopPadding;
int right1 = right - mRightBottomPadding;
int top1 = child.getBottom() + params.bottomMargin;
int bottom1 = top1 + mDividerSpacing;
canvas.drawRect(left1, top1, right1, bottom1, mPaint);
}
}
}

GridSpaceItemDecoration构造方法有两个:

GridSpaceItemDecoration(int spanCount, int spacing)
/**
* @param spanCount item 每行个数
* @param spacing item 间距
* @param includeEdge item 距屏幕周围是否也有间距
*/
public GridSpaceItemDecoration(int spanCount, int spacing, boolean includeEdge)

其他参数设置:

/**
* 设置从哪个位置 结束设置间距
*
* @param startFromSize 一般为HeaderView的个数 + 刷新布局(不一定设置)
* @param endFromSize 默认为1,一般为FooterView的个数 + 加载更多布局(不一定设置)
*/
public GridSpaceItemDecoration setNoShowSpace(int startFromSize, int endFromSize)

完整设置如下:

GridSpaceItemDecoration itemDecoration = new GridSpaceItemDecoration(3, 5, true)
.setNoShowSpace(1, 1);
recyclerView.addItemDecoration(itemDecoration);
核心代码
// 减掉不设置间距的position
position = position - mStartFromSize;
int column = position % mSpanCount;

// 瀑布流获取列方式不一样
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
column = ((StaggeredGridLayoutManager.LayoutParams) layoutParams).getSpanIndex();
}

if (mIncludeEdge) {// 屏幕四周有边距
/*
*示例:
* spacing = 10 ;spanCount = 3
* ---------10--------
* 10 3+7 6+4 10
* ---------10--------
* 10 3+7 6+4 10
* ---------10--------
*/
outRect.left = mSpacing - column * mSpacing / mSpanCount;
outRect.right = (column + 1) * mSpacing / mSpanCount;

if (position < mSpanCount) {
outRect.top = mSpacing;
}
outRect.bottom = mSpacing;

} else {
/*
*示例:
* spacing = 10 ;spanCount = 3
* --------0--------
* 0 3+7 6+4 0
* -------10--------
* 0 3+7 6+4 0
* --------0--------
*/
outRect.left = column * mSpacing / mSpanCount;
outRect.right = mSpacing - (column + 1) * mSpacing / mSpanCount;
if (position >= mSpanCount) {
outRect.top = mSpacing;
}
}
完整代码

SpacesItemDecoration

/**
* 给 LinearLayoutManager 增加分割线,可设置去除首尾分割线个数
*
* @author jingbin
* https://github.com/youlookwhat/ByRecyclerView
*/
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {

public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private static final String TAG = "itemDivider";
private Context mContext;
private Drawable mDivider;
private Rect mBounds = new Rect();
/**
* 在AppTheme里配置 android:listDivider
*/
private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
/**
* 头部 不显示分割线的item个数 这里应该包含刷新头,
* 比如有一个headerView和有下拉刷新,则这里传 2
*/
private int mHeaderNoShowSize = 0;
/**
* 尾部 不显示分割线的item个数 默认不显示最后一个item的分割线
*/
private int mFooterNoShowSize = 1;
/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
*/
private int mOrientation;
private Paint mPaint;
/**
* 如果是横向 - 宽度
* 如果是纵向 - 高度
*/
private int mDividerSpacing;
/**
* 如果是横向 - 左边距
* 如果是纵向 - 上边距
*/
private int mLeftTopPadding;
/**
* 如果是横向 - 右边距
* 如果是纵向 - 下边距
*/
private int mRightBottomPadding;
private ByRecyclerView byRecyclerView;

public SpacesItemDecoration(Context context) {
this(context, VERTICAL, 0, 1);
}

public SpacesItemDecoration(Context context, int orientation) {
this(context, orientation, 0, 1);
}

public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize) {
this(context, orientation, headerNoShowSize, 1);
}

/**
* Creates a divider {@link RecyclerView.ItemDecoration}
*
* @param context Current context, it will be used to access resources.
* @param orientation Divider orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}.
* @param headerNoShowSize headerViewSize + RefreshViewSize
* @param footerNoShowSize footerViewSize
*/
public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize, int footerNoShowSize) {
mContext = context;
mHeaderNoShowSize = headerNoShowSize;
mFooterNoShowSize = footerNoShowSize;
setOrientation(orientation);
final TypedArray a = context.obtainStyledAttributes(ATTRS);
mDivider = a.getDrawable(0);
a.recycle();
}

/**
* Sets the orientation for this divider. This should be called if
* {@link RecyclerView.LayoutManager} changes orientation.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public SpacesItemDecoration setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
}
mOrientation = orientation;
return this;
}

/**
* Sets the {@link Drawable} for this divider.
*
* @param drawable Drawable that should be used as a divider.
*/
public SpacesItemDecoration setDrawable(Drawable drawable) {
if (drawable == null) {
throw new IllegalArgumentException("drawable cannot be null.");
}
mDivider = drawable;
return this;
}

public SpacesItemDecoration setDrawable(@DrawableRes int id) {
setDrawable(ContextCompat.getDrawable(mContext, id));
return this;
}

@Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() == null || (mDivider == null && mPaint == null)) {
return;
}
if (mOrientation == VERTICAL) {
drawVertical(canvas, parent, state);
} else {
drawHorizontal(canvas, parent, state);
}
}

private void drawVertical(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
canvas.save();
final int left;
final int right;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
} else {
left = 0;
right = parent.getWidth();
}

final int childCount = parent.getChildCount();
final int lastPosition = state.getItemCount() - 1;
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int childRealPosition = parent.getChildAdapterPosition(child);

// 过滤到头部不显示的分割线
if (childRealPosition < mHeaderNoShowSize) {
continue;
}
// 过滤到尾部不显示的分割线
if (childRealPosition <= lastPosition - mFooterNoShowSize) {
if (mDivider != null) {
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}

if (mPaint != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int left1 = left + mLeftTopPadding;
int right1 = right - mRightBottomPadding;
int top1 = child.getBottom() + params.bottomMargin;
int bottom1 = top1 + mDividerSpacing;
canvas.drawRect(left1, top1, right1, bottom1, mPaint);
}
}
}
canvas.restore();
}

private void drawHorizontal(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
canvas.save();
final int top;
final int bottom;
//noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
if (parent.getClipToPadding()) {
top = parent.getPaddingTop();
bottom = parent.getHeight() - parent.getPaddingBottom();
canvas.clipRect(parent.getPaddingLeft(), top,
parent.getWidth() - parent.getPaddingRight(), bottom);
} else {
top = 0;
bottom = parent.getHeight();
}

final int childCount = parent.getChildCount();
final int lastPosition = state.getItemCount() - 1;
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final int childRealPosition = parent.getChildAdapterPosition(child);

// 过滤到头部不显示的分割线
if (childRealPosition < mHeaderNoShowSize) {
continue;
}
// 过滤到尾部不显示的分割线
if (childRealPosition <= lastPosition - mFooterNoShowSize) {
if (mDivider != null) {
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int right = mBounds.right + Math.round(child.getTranslationX());
final int left = right - mDivider.getIntrinsicWidth();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}

if (mPaint != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int left1 = child.getRight() + params.rightMargin;
int right1 = left1 + mDividerSpacing;
int top1 = top + mLeftTopPadding;
int bottom1 = bottom - mRightBottomPadding;
canvas.drawRect(left1, top1, right1, bottom1, mPaint);
}
}
}
canvas.restore();
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mDivider == null && mPaint == null) {
outRect.set(0, 0, 0, 0);
return;
}
//parent.getChildCount() 不能拿到item的总数
int lastPosition = state.getItemCount() - 1;
int position = parent.getChildAdapterPosition(view);

boolean mScrollTopFix = false;
if (byRecyclerView == null && parent instanceof ByRecyclerView) {
byRecyclerView = (ByRecyclerView) parent;
}
if (byRecyclerView != null && byRecyclerView.isRefreshEnabled()) {
mScrollTopFix = true;
}

// 滚动条置顶
boolean isFixScrollTop = mScrollTopFix && position == 0;
boolean isShowDivider = mHeaderNoShowSize <= position && position <= lastPosition - mFooterNoShowSize;

if (mOrientation == VERTICAL) {
if (isFixScrollTop) {
outRect.set(0, 0, 0, 1);
} else if (isShowDivider) {
outRect.set(0, 0, 0, mDivider != null ? mDivider.getIntrinsicHeight() : mDividerSpacing);
} else {
outRect.set(0, 0, 0, 0);
}
} else {
if (isFixScrollTop) {
outRect.set(0, 0, 1, 0);
} else if (isShowDivider) {
outRect.set(0, 0, mDivider != null ? mDivider.getIntrinsicWidth() : mDividerSpacing, 0);
} else {
outRect.set(0, 0, 0, 0);
}
}
}

/**
* 设置不显示分割线的item位置与个数
*
* @param headerNoShowSize 头部 不显示分割线的item个数
* @param footerNoShowSize 尾部 不显示分割线的item个数,默认1,不显示最后一个,最后一个一般为加载更多view
*/
public SpacesItemDecoration setNoShowDivider(int headerNoShowSize, int footerNoShowSize) {
this.mHeaderNoShowSize = headerNoShowSize;
this.mFooterNoShowSize = footerNoShowSize;
return this;
}

/**
* 设置不显示头部分割线的item个数
*
* @param headerNoShowSize 头部 不显示分割线的item个数
*/
public SpacesItemDecoration setHeaderNoShowDivider(int headerNoShowSize) {
this.mHeaderNoShowSize = headerNoShowSize;
return this;
}

public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing) {
return setParam(dividerColor, dividerSpacing, 0, 0);
}

/**
* 直接设置分割线颜色等,不设置drawable
*
* @param dividerColor 分割线颜色
* @param dividerSpacing 分割线间距
* @param leftTopPaddingDp 如果是横向 - 左边距
* 如果是纵向 - 上边距
* @param rightBottomPaddingDp 如果是横向 - 右边距
* 如果是纵向 - 下边距
*/
public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing, float leftTopPaddingDp, float rightBottomPaddingDp) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(ContextCompat.getColor(mContext, dividerColor));
mDividerSpacing = dividerSpacing;
mLeftTopPadding = dip2px(leftTopPaddingDp);
mRightBottomPadding = dip2px(rightBottomPaddingDp);
mDivider = null;
return this;
}

/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
public int dip2px(float dpValue) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}

}

GridSpaceItemDecoration:

/**
* 给 GridLayoutManager or StaggeredGridLayoutManager 设置间距,可设置去除首尾间距个数
*
* @author jingbin
* https://github.com/youlookwhat/ByRecyclerView
*/

public class GridSpaceItemDecoration extends RecyclerView.ItemDecoration {

/**
* 每行个数
*/
private int mSpanCount;
/**
* 间距
*/
private int mSpacing;
/**
* 距屏幕周围是否也有间距
*/
private boolean mIncludeEdge;

/**
* 头部 不显示间距的item个数
*/
private int mStartFromSize;
/**
* 尾部 不显示间距的item个数 默认不处理最后一个item的间距
*/
private int mEndFromSize = 1;

public GridSpaceItemDecoration(int spanCount, int spacing) {
this(spanCount, spacing, true);
}

/**
* @param spanCount item 每行个数
* @param spacing item 间距
* @param includeEdge item 距屏幕周围是否也有间距
*/
public GridSpaceItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.mSpanCount = spanCount;
this.mSpacing = spacing;
this.mIncludeEdge = includeEdge;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int lastPosition = state.getItemCount() - 1;
int position = parent.getChildAdapterPosition(view);
if (mStartFromSize <= position && position <= lastPosition - mEndFromSize) {

// 减掉不设置间距的position
position = position - mStartFromSize;
int column = position % mSpanCount;

// 瀑布流获取列方式不一样
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
column = ((StaggeredGridLayoutManager.LayoutParams) layoutParams).getSpanIndex();
}

if (mIncludeEdge) {
/*
*示例:
* spacing = 10 ;spanCount = 3
* ---------10--------
* 10 3+7 6+4 10
* ---------10--------
* 10 3+7 6+4 10
* ---------10--------
*/
outRect.left = mSpacing - column * mSpacing / mSpanCount;
outRect.right = (column + 1) * mSpacing / mSpanCount;

if (position < mSpanCount) {
outRect.top = mSpacing;
}
outRect.bottom = mSpacing;

} else {
/*
*示例:
* spacing = 10 ;spanCount = 3
* --------0--------
* 0 3+7 6+4 0
* -------10--------
* 0 3+7 6+4 0
* --------0--------
*/
outRect.left = column * mSpacing / mSpanCount;
outRect.right = mSpacing - (column + 1) * mSpacing / mSpanCount;
if (position >= mSpanCount) {
outRect.top = mSpacing;
}
}
}
}

/**
* 设置从哪个位置 开始设置间距
*
* @param startFromSize 一般为HeaderView的个数 + 刷新布局(不一定设置)
*/
public GridSpaceItemDecoration setStartFrom(int startFromSize) {
this.mStartFromSize = startFromSize;
return this;
}

/**
* 设置从哪个位置 结束设置间距。默认为1,默认用户设置了上拉加载
*
* @param endFromSize 一般为FooterView的个数 + 加载更多布局(不一定设置)
*/
public GridSpaceItemDecoration setEndFromSize(int endFromSize) {
this.mEndFromSize = endFromSize;
return this;
}

/**
* 设置从哪个位置 结束设置间距
*
* @param startFromSize 一般为HeaderView的个数 + 刷新布局(不一定设置)
* @param endFromSize 默认为1,一般为FooterView的个数 + 加载更多布局(不一定设置)
*/
public GridSpaceItemDecoration setNoShowSpace(int startFromSize, int endFromSize) {
this.mStartFromSize = startFromSize;
this.mEndFromSize = endFromSize;
return this;
}
}

总结一下

这两个类SpacesItemDecorationGridSpaceItemDecoration基本涵盖了所有列表的情况,如果有一些特殊的需求在上面稍微拓展一下就好,它们收录在本人开源的一个  RecyclerView开源库里:youlookwhat/ByRecyclerView`。如有其他问题,欢迎留言骚扰~

---END---

推荐阅读:
【译】一文带你了解Android中23个关于Canvas绘制的方法
一文形象生动地解释什么是MVP架构模式?
【超级实用】Iterm2 + oh-my-zsh 打造强大的终端编辑器
自定义View - 仿华为LoadingView
你好, View Binding! 再次再见, findViewById!
Android Studio 3.6 稳定版发布啦,快来围观!
70650832dfcea61877eb32885e1cdf77.png
每一个“在看”,我都当成真的喜欢589fba9e6c7c86175074da330ba559eb.gif
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值