我基本上找遍了网上全部经过ItemDecorationd
设置分隔线的文章,但都不尽如意,它们大多只适用于部分状况,好比只能给线性布局设置、只能设置color不能设置drawable、不能去除HeaderView部分的分割线、配置麻烦等等等。java
因而我费尽周折出了两个类:SpacesItemDecoration,GridSpaceItemDecoration。它们基本解决了上述全部问题!android
SpacesItemDecoration:git
给LinearLayoutManager设置
GridSpaceItemDecoration:github
给GridLayoutManager或StaggeredGridLayoutManager设置
绘制原理:canvas
网上不少解释经过ItemDecoration绘制分割线的原理的文章,我简单总结一下,在getItemOffsets()
方法里设置item宽度的偏移量,在onDraw()
方法里主要绘制分割线颜色。getItemOffsets 是针对每个 ItemView,而 onDraw 方法倒是针对 RecyclerView 自己,因此在 onDraw 方法中须要遍历屏幕上可见的 ItemView,分别获取它们的位置信息,而后分别的绘制对应的分割线。 -- 参考:https://juejin.im/post/5cecef...ide
示例图:布局
SpacesItemDecoration | GridSpaceItemDecoration |
---|---|
![]() |
![]() |
构造方法有四个:post
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
只能选择其一:this
/** * 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)
一个完整的设置以下:spa
// 设置分割线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(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; } }
这两个类SpacesItemDecoration、GridSpaceItemDecoration 基本涵盖了全部列表的状况,若是有一些特殊的需求在上面稍微拓展一下就好,它们收录在本人开源的一个RecyclerView开源库里:youlookwhat/ByRecyclerView。若有其余问题,欢迎留言骚扰~