ByRecyclerView: true universal split line (linear / grid / waterfall flow)

Keywords: github Android Mobile

Preface

I've basically searched all the articles on the Internet that set the separator line through itemdecorated, but they are not satisfactory. Most of them are only applicable to some situations, such as setting only for linear layout, setting only color, setting drawable, not removing the separator line of HeaderView, configuration trouble, etc.

So I went to great lengths to come up with two classes: SpacesItemDecorationGridSpaceItemDecoration . They basically solve all the above problems!

What are their functions

SpacesItemDecoration:

Setting up for LinearLayoutManager

  • 1. color or drawable can be set
  • 2. You can set the spacing between the left and right or up and down of the split line
  • 3. It can be set that the header or footer does not display the number of split lines. The function is similar to the setheaderdividerenabled (ture) of ListView
  • 4. Support horizontal or vertical

GridSpaceItemDecoration:

Set GridLayoutManager or stacgeredgridlayoutmanager

  • 1. It can be configured to display split lines only around
  • 2. You can set the number of split lines that the header or footer does not display

Drawing principle:

There are many articles on the Internet explaining the principle of drawing split lines through ItemDecoration. I will briefly summarize that the offset of item width is set in getItemOffsets() method, and the color of split lines is mainly drawn in onDraw() method. getItemOffsets is for each ItemView, while onDraw method is for RecyclerView itself. Therefore, in onDraw method, you need to traverse the itemviews visible on the screen, obtain their location information, and draw the corresponding split lines respectively. – reference: https://juejin.im/post/5cecef7d5188250b3a1b9173

Example:

SpacesItemDecoration GridSpaceItemDecoration

Parameter configuration

SpacesItemDecoration

There are four construction methods:

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      Horizontal direction or vertical direction, the default is spacesitedecoration.vertical
 * @param headerNoShowSize The number of item s that do not display the divider should include the refresh header here
 * @param footerNoShowSize The number of items with no split line displayed at the end does not display the split line of the last item by default
 */
public SpacesItemDecoration(Context context, int orientation, int headerNoShowSize, int footerNoShowSize)

Other parameter settings, in which setDrawable and setParam can only choose one:

/**
 * Sets the {@link Drawable} for this divider.
 *
 * @param drawable Drawable that should be used as a divider.
 */
public SpacesItemDecoration setDrawable(Drawable drawable)

/**
 * Directly set the color of the split line, etc., without setting drawable
 *
 * @param dividerColor         Split line color
 * @param dividerSpacing       Split line spacing
 * @param leftTopPaddingDp     If transverse left margin
 *                             If it's longitudinal - top margin
 * @param rightBottomPaddingDp If transverse right margin
 *                             If it's longitudinal - bottom margin
 */
public SpacesItemDecoration setParam(int dividerColor, int dividerSpacing, float leftTopPaddingDp, float rightBottomPaddingDp)

A complete setup is as follows:

// Set split line color
SpacesItemDecoration itemDecoration = new SpacesItemDecoration(recyclerView.getContext(), SpacesItemDecoration.VERTICAL, 0, 1)
    .setParam(R.color.colorLine, 1, 12, 12);
recyclerView.addItemDecoration(itemDecoration);
        
// Set drawable
SpacesItemDecoration itemDecoration = new SpacesItemDecoration(recyclerView.getContext(), SpacesItemDecoration.VERTICAL, 0, 1)
    .setDrawable(R.drawable.shape_line);
recyclerView.addItemDecoration(itemDecoration);

Core code

Here we mainly explain the core code of these parameter configurations. For details, please refer to the source code:

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

    // Filter to split lines not shown in the head
    if (childRealPosition < mHeaderNoShowSize) {
        continue;
    }
    // Filter to split lines not shown at tail
    if (childRealPosition <= lastPosition - mFooterNoShowSize) {

        // Set 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);
        }

        // Set color
        if (mPaint != null) {
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            // Head and tail spacing
            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

There are two construction methods:

GridSpaceItemDecoration(int spanCount, int spacing)
/**
 * @param spanCount   item Number of rows per row
 * @param spacing     item spacing
 * @param includeEdge item Is there a gap around the screen
 */
public GridSpaceItemDecoration(int spanCount, int spacing, boolean includeEdge)

Other parameter settings:

/**
 * Set where to end set spacing
 *
 * @param startFromSize Generally, the number of headerviews + refresh layout (not necessarily set)
 * @param endFromSize   The default value is 1, which is generally the number of footerviews + load more layouts (not necessarily set)
 */
public GridSpaceItemDecoration setNoShowSpace(int startFromSize, int endFromSize)

The complete settings are as follows:

GridSpaceItemDecoration itemDecoration = new GridSpaceItemDecoration(3, 5, true)
        .setNoShowSpace(1, 1);
recyclerView.addItemDecoration(itemDecoration);

Core code

// Subtract the position without spacing
position = position - mStartFromSize;
int column = position % mSpanCount;

// Different ways to get columns for waterfall flow
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
    column = ((StaggeredGridLayoutManager.LayoutParams) layoutParams).getSpanIndex();
}

if (mIncludeEdge) {// There are margins around the screen
    /*
     *Example:
     * 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 {
    /*
     *Example:
     * 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;
    }
}

Complete code

SpacesItemDecoration:

/**
 * Add split lines to LinearLayoutManager to set the number of split lines to be removed
 *
 * @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();
    /**
     * Configure android:listDivider in AppTheme
     */
    private static final int[] ATTRS = new int[]{android.R.attr.listDivider};
    /**
     * The number of item s that do not display the split line in the header should include the refresh header,
     * For example, if there is a headerView and there is a pull-down refresh, then here pass 2
     */
    private int mHeaderNoShowSize = 0;
    /**
     * The number of items with no split line displayed at the end does not display the split line of the last item by default
     */
    private int mFooterNoShowSize = 1;
    /**
     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}.
     */
    private int mOrientation;
    private Paint mPaint;
    /**
     * If transverse width
     * In case of longitudinal height
     */
    private int mDividerSpacing;
    /**
     * If transverse left margin
     * If it's longitudinal - top margin
     */
    private int mLeftTopPadding;
    /**
     * If transverse right margin
     * If it's longitudinal - bottom margin
     */
    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);

            // Filter to split lines not shown in the head
            if (childRealPosition < mHeaderNoShowSize) {
                continue;
            }
            // Filter to split lines not shown at tail
            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);

            // Filter to split lines not shown in the head
            if (childRealPosition < mHeaderNoShowSize) {
                continue;
            }
            // Filter to split lines not shown at tail
            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() cannot get the total number of item s
        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;
        }

        // Scroll bar top
        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);
            }
        }
    }

    /**
     * Set the location and number of item s that do not display split lines
     *
     * @param headerNoShowSize Number of item s with no split line displayed in the header
     * @param footerNoShowSize The number of item s of the split line is not displayed at the end. The default is 1, and the last one is not displayed. The last one is generally to load more view s
     */
    public SpacesItemDecoration setNoShowDivider(int headerNoShowSize, int footerNoShowSize) {
        this.mHeaderNoShowSize = headerNoShowSize;
        this.mFooterNoShowSize = footerNoShowSize;
        return this;
    }

    /**
     * Set the number of item s that do not display the header divider
     *
     * @param headerNoShowSize Number of item s with no split line displayed in the header
     */
    public SpacesItemDecoration setHeaderNoShowDivider(int headerNoShowSize) {
        this.mHeaderNoShowSize = headerNoShowSize;
        return this;
    }

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

    /**
     * Directly set the color of the split line, etc., without setting drawable
     *
     * @param dividerColor         Split line color
     * @param dividerSpacing       Split line spacing
     * @param leftTopPaddingDp     If transverse left margin
     *                             If it's longitudinal - top margin
     * @param rightBottomPaddingDp If transverse right margin
     *                             If it's longitudinal - bottom margin
     */
    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;
    }

    /**
     * Change from dp unit to PX (pixel) according to the resolution of mobile phone
     */
    public int dip2px(float dpValue) {
        final float scale = mContext.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    
}

GridSpaceItemDecoration:

/**
 * Set the spacing for gridlayoutmanager or stacgeredgridlayoutmanager to remove the number of head and tail spacing
 *
 * @author jingbin
 * https://github.com/youlookwhat/ByRecyclerView
 */

public class GridSpaceItemDecoration extends RecyclerView.ItemDecoration {

    /**
     * Number of rows per row
     */
    private int mSpanCount;
    /**
     * spacing
     */
    private int mSpacing;
    /**
     * Is there a gap around the screen
     */
    private boolean mIncludeEdge;

    /**
     * Number of item s with no spacing in the header
     */
    private int mStartFromSize;
    /**
     * The number of items with no spacing at the end does not process the spacing of the last item by default
     */
    private int mEndFromSize = 1;

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

    /**
     * @param spanCount   item Number of rows per row
     * @param spacing     item spacing
     * @param includeEdge item Is there a gap around the screen
     */
    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) {

            // Subtract the position without spacing
            position = position - mStartFromSize;
            int column = position % mSpanCount;

            // Different ways to get columns for waterfall flow
            ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
            if (layoutParams instanceof StaggeredGridLayoutManager.LayoutParams) {
                column = ((StaggeredGridLayoutManager.LayoutParams) layoutParams).getSpanIndex();
            }

            if (mIncludeEdge) {
                /*
                 *Example:
                 * 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 {
                /*
                 *Example:
                 * 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;
                }
            }
        }
    }

    /**
     * Set where to start spacing
     *
     * @param startFromSize Generally, the number of headerviews + refresh layout (not necessarily set)
     */
    public GridSpaceItemDecoration setStartFrom(int startFromSize) {
        this.mStartFromSize = startFromSize;
        return this;
    }

    /**
     * Sets where the spacing ends. The default is 1, and the default user has set pull-up loading
     *
     * @param endFromSize Generally, the number of footerviews + load more layouts (not necessarily set)
     */
    public GridSpaceItemDecoration setEndFromSize(int endFromSize) {
        this.mEndFromSize = endFromSize;
        return this;
    }

    /**
     * Set where to end set spacing
     *
     * @param startFromSize Generally, the number of headerviews + refresh layout (not necessarily set)
     * @param endFromSize   The default value is 1, which is generally the number of footerviews + load more layouts (not necessarily set)
     */
    public GridSpaceItemDecoration setNoShowSpace(int startFromSize, int endFromSize) {
        this.mStartFromSize = startFromSize;
        this.mEndFromSize = endFromSize;
        return this;
    }
}

Sum up

These two categories SpacesItemDecoration,GridSpaceItemDecoration It basically covers all the lists. If there are some special needs to be expanded a little, they will be included in a RecyclerView open source library of my own: youlookwhat/ByRecyclerView . If you have any other questions, please leave a message~

89 original articles published, 49 praised, 110000 visitors+
Private letter follow

Posted by thinkgfx on Sat, 22 Feb 2020 22:35:37 -0800