我基本上找遍了網上全部經過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。若有其餘問題,歡迎留言騷擾~