ByRecyclerView:真·萬能分割線 (線性/宮格/瀑布流)

前言

我基本上找遍了網上全部經過ItemDecoration設置分隔線的文章,但都不盡如意,它們大多隻適用於部分狀況,好比只能給線性佈局設置、只能設置color不能設置drawable、不能去除HeaderView部分的分割線、配置麻煩等等等。java

因而我費盡周折出了兩個類:SpacesItemDecorationGridSpaceItemDecoration。它們基本解決了上述全部問題!android

它們有什麼功能

SpacesItemDecorationgit

給LinearLayoutManager設置github

  • 一、可設置color或drawable
  • 二、可設置分割線左右或上下的間距
  • 三、可設置header或footer不顯示分割線的個數,功能似ListView的setHeaderDividersEnabled(ture)
  • 四、支持橫向或縱向

GridSpaceItemDecorationcanvas

給GridLayoutManager或StaggeredGridLayoutManager設置ide

  • 一、可配置只在四周是否顯示分割線
  • 二、可設置header或footer不顯示分割線的個數

繪製原理佈局

網上不少解釋經過ItemDecoration繪製分割線的原理的文章,我簡單總結一下,在getItemOffsets()方法裏設置item寬度的偏移量,在onDraw()方法裏主要繪製分割線顏色。getItemOffsets 是針對每個 ItemView,而 onDraw 方法倒是針對 RecyclerView 自己,因此在 onDraw 方法中須要遍歷屏幕上可見的 ItemView,分別獲取它們的位置信息,而後分別的繪製對應的分割線。 -- 參考:juejin.im/post/5cecef…post

示例圖this

SpacesItemDecoration GridSpaceItemDecoration

參數配置

SpacesItemDecoration

構造方法有四個:spa

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) 複製代碼

其餘參數設置,其中setDrawablesetParam只能選擇其一:

/** * 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。若有其餘問題,歡迎留言騷擾~

相關文章
相關標籤/搜索