49

What's the best and easiest way to decorate RecyclerView to have such look & feel?

enter image description here

The main challenge here is having dividers only between items, but not between items and left/right borders of screen.

Any ideas?

AlexKorovyansky
  • 4,873
  • 5
  • 37
  • 48
  • see https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ItemDecoration.html#getItemOffsets(android.graphics.Rect, android.view.View, android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State) – pskink Mar 19 '15 at 14:19
  • 1
    @pskink I see that it's necessary to create/reuse some kind of item decoration. The question is about concrete solution. – AlexKorovyansky Mar 19 '15 at 14:20
  • @pskink what's correct way to determine wether item has right / left neighbours or not within getItemOffsets method? – AlexKorovyansky Mar 19 '15 at 14:39
  • RecyclerView.getChildPosition(View child) gives you the item position, and the position determines where the item is lay out on the RecyclerView – pskink Mar 19 '15 at 14:46
  • @pskink how to understand is it around border or not (in general case) ? – AlexKorovyansky Mar 19 '15 at 16:37

10 Answers10

116

I don't know why do you need that, but this UI is quite easy to implement with RecyclerView decorator.

<!--Integer Value that number of column in RecyclerView-->
<integer name="photo_list_preview_columns">3</integer>

<!-- inter spacing between RecyclerView's Item-->
<dimen name="photos_list_spacing">10dp</dimen>

You can change photo_list_preview_columns and photos_list_spacing according to your needs.

mRecylerView.addItemDecoration(new ItemDecorationAlbumColumns(
    getResources().getDimensionPixelSize(R.dimen.photos_list_spacing), 
    getResources().getInteger(R.integer.photo_list_preview_columns)));

and decorator (needs some refatoring)

import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.view.View;

public class ItemDecorationAlbumColumns extends RecyclerView.ItemDecoration {

    private int mSizeGridSpacingPx;
    private int mGridSize;

    private boolean mNeedLeftSpacing = false;

    public ItemDecorationAlbumColumns(int gridSpacingPx, int gridSize) {
        mSizeGridSpacingPx = gridSpacingPx;
        mGridSize = gridSize;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int frameWidth = (int) ((parent.getWidth() - (float) mSizeGridSpacingPx * (mGridSize - 1)) / mGridSize);
        int padding = parent.getWidth() / mGridSize - frameWidth;
        int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();
        if (itemPosition < mGridSize) {
            outRect.top = 0;
        } else {
            outRect.top = mSizeGridSpacingPx;
        }
        if (itemPosition % mGridSize == 0) {
            outRect.left = 0;
            outRect.right = padding;
            mNeedLeftSpacing = true;
        } else if ((itemPosition + 1) % mGridSize == 0) {
            mNeedLeftSpacing = false;
            outRect.right = 0;
            outRect.left = padding;
        } else if (mNeedLeftSpacing) {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx - padding;
            if ((itemPosition + 2) % mGridSize == 0) {
                outRect.right = mSizeGridSpacingPx - padding;
            } else {
                outRect.right = mSizeGridSpacingPx / 2;
            }
        } else if ((itemPosition + 2) % mGridSize == 0) {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx / 2;
            outRect.right = mSizeGridSpacingPx - padding;
        } else {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx / 2;
            outRect.right = mSizeGridSpacingPx / 2;
        }
        outRect.bottom = 0;
    }
}

enter image description here enter image description here

Nil
  • 312
  • 4
  • 19
Dmitry L.
  • 1,572
  • 1
  • 12
  • 10
  • @AlexKorovyansky the same UI but with Load more items howthis can be achieved ? – Erum Aug 17 '15 at 04:49
  • This is a good start but this draw lines underneath the bottom row when items are animating (i.e., notifyItemChanged called) – Greg Ennis Jul 20 '16 at 14:46
  • 1
    If I could I would just come and hug you bro. This is the thing I was looking for. Thanks – viper Jan 10 '17 at 10:18
  • What is mean of mNeedLeftSpacing state variable? Does it relay on call ordering? – Vladimir Berezkin Aug 13 '18 at 15:12
  • How to add color into divider? – Waqar Vicky Oct 19 '20 at 17:33
  • Thanks! You can remove `frameWidth` and set `int padding = mSizeGridSpacingPx - mSizeGridSpacingPx / mGridSize;` – CoolMind Dec 19 '20 at 20:31
  • @WaqarVicky, see [Yeldar.N solution](https://stackoverflow.com/a/64682905/2914140) with drawable and `onDraw`. – CoolMind Dec 19 '20 at 20:33
  • if you're using ConcatAdapter then **getViewAdapterPosition()** will return individual adapters item position which is wrong. for this we need to use **getAbsoluteAdapterPosition()** which will return the actual position respect to RecyclerView. – Bhavin May 27 '21 at 11:37
12

Here's a simpler and more user-friendly implementation:

public class MediaSpaceDecoration extends RecyclerView.ItemDecoration {
    private final int spacing;
    private final List<Integer> allowedViewTypes = Arrays.asList(
            R.layout.item_image,
            R.layout.item_blur);

    public MediaSpaceDecoration(int spacing) {
        this.spacing = spacing;
    }

    @Override
    public void getItemOffsets(Rect outRect,
                               View view,
                               RecyclerView parent,
                               RecyclerView.State state) {
        final int position = parent.getChildAdapterPosition(view);
        if (!isMedia(parent, position)) {
            return;
        }

        final int totalSpanCount = getTotalSpanCount(parent);
        int spanSize = getItemSpanSize(parent, position);
        if (totalSpanCount == spanSize) {
            return;
        }

        outRect.top = isInTheFirstRow(position, totalSpanCount) ? 0 : spacing;
        outRect.left = isFirstInRow(position, totalSpanCount) ? 0 : spacing / 2;
        outRect.right = isLastInRow(position, totalSpanCount) ? 0 : spacing / 2;
        outRect.bottom = 0; // don't need
    }

    private boolean isInTheFirstRow(int position, int spanCount) {
        return position < spanCount;
    }

    private boolean isFirstInRow(int position, int spanCount) {
        return position % spanCount == 0;
    }

    private boolean isLastInRow(int position, int spanCount) {
        return isFirstInRow(position + 1, spanCount);
    }

    private int getTotalSpanCount(RecyclerView parent) {
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        return layoutManager instanceof GridLayoutManager
            ? ((GridLayoutManager) layoutManager).getSpanCount()
            : 1;
    }

    private int getItemSpanSize(RecyclerView parent, int position) {
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        return layoutManager instanceof GridLayoutManager
            ? ((GridLayoutManager) layoutManager).getSpanSizeLookup().getSpanSize(position)
            : 1;
    }

    private boolean isMedia(RecyclerView parent, int viewPosition) {
        final RecyclerView.Adapter adapter = parent.getAdapter();
        final int viewType = adapter.getItemViewType(viewPosition);
        return allowedViewTypes.contains(viewType);
    }
}

I also check before setting the outRect because I have various spanSizes for each viewType and I need to add an extra middle-space only for allowedViewTypes. You can easily remove that verification and the code would be even simpler. It looks like this for me: GridLayout with a different spanSize for each viewType

Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
Nikolay Kulachenko
  • 4,604
  • 4
  • 31
  • 37
  • Thanks. I took out the allowedViewTypes and is Media and also set outRect.top to 0 because I wanted only a space in the middle between columns, not between rows. – Sam Jul 20 '20 at 19:04
  • 1
    This doesn't work for a grid > 2 images wide as then you can't just divide the spacing by 2 and it depends on the position of the image. Further details in my answer below: https://stackoverflow.com/a/63488804/238166 – Inti Aug 19 '20 at 14:10
  • What are `item_image`, `item_blur`? – CoolMind Dec 12 '20 at 18:39
6

You may got your answer but i'm still posting my solution that can help others. This can be used for vertical, horizontal lists or grid views by passing the orientation.

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };

    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    public static final int GRID = 2;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST && orientation != GRID) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else if(mOrientation == HORIZONTAL_LIST){
            drawHorizontal(c, parent);
        } else {
            drawVertical(c, parent);
            drawHorizontal(c, parent);
        }
    }

    public void drawVertical(Canvas c, RecyclerView parent) {
        if (parent.getChildCount() == 0) return;

        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final View child = parent.getChildAt(0);
        if (child.getHeight() == 0) return;

        final RecyclerView.LayoutParams params =
                (RecyclerView.LayoutParams) child.getLayoutParams();
        int top = child.getBottom() + params.bottomMargin + mDivider.getIntrinsicHeight();
        int bottom = top + mDivider.getIntrinsicHeight();

        final int parentBottom = parent.getHeight() - parent.getPaddingBottom();
        while (bottom < parentBottom) {
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);

            top += mDivider.getIntrinsicHeight() + params.topMargin + child.getHeight() + params.bottomMargin + mDivider.getIntrinsicHeight();
            bottom = top + mDivider.getIntrinsicHeight();
        }
    }

    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params =
                    (RecyclerView.LayoutParams) child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin + mDivider.getIntrinsicHeight();
            final int right = left + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else if(mOrientation == HORIZONTAL_LIST) {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), mDivider.getIntrinsicHeight());
        }
    }
}
Nauman Zubair
  • 1,208
  • 15
  • 27
4

Here's my implementation in Kotlin. I modified code from other answers so that divider is shown also for full spanned items.

import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView

class GridDividerItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)

        val totalSpanCount = getTotalSpanCount(parent)
        val spanSize = getItemSpanSize(parent, position)

        outRect.top = if (isInTheFirstRow(position, totalSpanCount)) 0 else spacing
        outRect.left = if (isFirstInRow(position, totalSpanCount, spanSize)) 0 else spacing / 2
        outRect.right = if (isLastInRow(position, totalSpanCount, spanSize)) 0 else spacing / 2
        outRect.bottom = 0
    }

    private fun isInTheFirstRow(position: Int, totalSpanCount: Int): Boolean =
        position < totalSpanCount

    private fun isFirstInRow(position: Int, totalSpanCount: Int, spanSize: Int): Boolean =
        if (totalSpanCount != spanSize) {
            position % totalSpanCount == 0
        } else true

    private fun isLastInRow(position: Int, totalSpanCount: Int, spanSize: Int): Boolean =
        isFirstInRow(position + 1, totalSpanCount, spanSize)

    private fun getTotalSpanCount(parent: RecyclerView): Int =
        (parent.layoutManager as? GridLayoutManager)?.spanCount ?: 1

    private fun getItemSpanSize(parent: RecyclerView, position: Int): Int =
        (parent.layoutManager as? GridLayoutManager)?.spanSizeLookup?.getSpanSize(position) ?: 1
}

Result:

grid_divider_icon_decoration

Micer
  • 8,731
  • 3
  • 79
  • 73
  • 1
    What if there is a full spanned item at 0 position ? The defined methods isInTheFirstRow, isFirstInRow, isLastInRow doesn't seem generic. Is it intended only for the specific use case of yours as shown in the image or a generic one ? – rd7773 Oct 22 '19 at 06:47
  • It's supposed to be generic, but you're right, I think full spanned item at 0 position is not covered. I didn't try though. – Micer Oct 22 '19 at 09:52
  • Also, how do you set custom color these decoration lines ? Not for all dividers in the app (android:listDivider), just for this recyclerview ? Any idea ? – rd7773 Oct 22 '19 at 13:07
  • You can add `@drawable/divider_grid` to style of your RecyclerView. – Micer Oct 24 '19 at 07:47
  • This will apply to all dividers in the app, we need to apply just for the specific decorator only. – rd7773 Oct 31 '19 at 12:06
  • How you add that blue color in divider? – Waqar Vicky Oct 19 '20 at 17:32
  • @WaqarVicky it's a background color of RecyclerView – Micer Oct 20 '20 at 09:02
3

One more simple solution that worked for me. Hope it can be useful.

class GridItemDecorator(val context: Context, private val spacingDp: Int, private val mGridSize: Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {

        val resources = context.resources
        val spacingPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, spacingDp.toFloat(), resources.displayMetrics)

        val bit = if (spacingPx > mGridSize) Math.round(spacingPx / mGridSize) else 1
        val itemPosition = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition

        outRect.top = if (itemPosition < mGridSize) 0 else  bit * mGridSize
        outRect.bottom = 0

        val rowPosition = itemPosition % mGridSize
        outRect.left = rowPosition * bit
        outRect.right = (mGridSize - rowPosition - 1) * bit

    }
}
AlexKost
  • 2,792
  • 4
  • 22
  • 42
1
  @BindingAdapter({"bind:adapter"})
    public static void bind(RecyclerView view, RecyclerView.Adapter<BaseViewHolder> adapter) {
        view.setLayoutManager(new GridLayoutManager(view.getContext(), 3));
        view.addItemDecoration(new SpacesItemDecorationGrid(view.getContext(), 4, 3));
        view.setItemAnimator(new DefaultItemAnimator());
        view.setAdapter(adapter);
    }


public class SpacesItemDecorationGrid extends RecyclerView.ItemDecoration {

    private int mSizeGridSpacingPx;
    private int mGridSize;
    private boolean mNeedLeftSpacing = false;

    /**
     * @param gridSpacingPx
     * @param gridSize
     */
    SpacesItemDecorationGrid(Context context, int gridSpacingPx, int gridSize) {
        mSizeGridSpacingPx = (int) Util.convertDpToPixel(gridSpacingPx, context);
        mGridSize = gridSize;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int frameWidth = (int) ((parent.getWidth() - (float) mSizeGridSpacingPx * (mGridSize - 1)) / mGridSize);
        int padding = parent.getWidth() / mGridSize - frameWidth;
        int itemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewAdapterPosition();
        int itemCount = parent.getAdapter().getItemCount() - mGridSize;


   /*     if (itemPosition < mGridSize) {
            outRect.top = mSizeGridSpacingPx;
        } else {
                   outRect.top = mSizeGridSpacingPx;
        }*/
        outRect.top = mSizeGridSpacingPx;
        if (itemPosition % mGridSize == 0) {
            outRect.left = mSizeGridSpacingPx;
            outRect.right = padding;
            mNeedLeftSpacing = true;
        } else if ((itemPosition + 1) % mGridSize == 0) {
            mNeedLeftSpacing = false;
            outRect.right = mSizeGridSpacingPx;
            outRect.left = padding;
        } else if (mNeedLeftSpacing) {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx - padding;
            if ((itemPosition + 2) % mGridSize == 0) {
                outRect.right = mSizeGridSpacingPx - padding;
            } else {
                outRect.right = mSizeGridSpacingPx / 2;
            }
        } else if ((itemPosition + 2) % mGridSize == 0) {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx / 2;
            outRect.right = mSizeGridSpacingPx - padding;
        } else {
            mNeedLeftSpacing = false;
            outRect.left = mSizeGridSpacingPx / 2;
            outRect.right = mSizeGridSpacingPx / 2;
        }
        if (itemPosition > itemCount) {
            outRect.bottom = mSizeGridSpacingPx;
        } else {
            outRect.bottom = 0;
        }

    }

}
  • 2
    Please consider adding some explanation of what the code does. Code-only answers _may_ technically answer the question but are less likely to be useful to other users with similar problems. – Peter Hall May 13 '17 at 16:52
1

Below is my custom class that allows equal spacing between grid cells in Kotlin:

class GridItemOffsetDecoration(private val spanCount: Int, private var mItemOffset: Int) : ItemDecoration() {

override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
                            state: RecyclerView.State) {
    val position = parent.getChildAdapterPosition(view)

    if (position < spanCount) {
        if (position % 2 == 0) { // left grid
            outRect.set(0, mItemOffset, mItemOffset / 2, mItemOffset / 2)
        } else { // right grid
            outRect.set(mItemOffset / 2, mItemOffset, 0, mItemOffset / 2)
        }

    } else if (position % 2 == 0) { // left grid
        outRect.set(0, mItemOffset / 2, mItemOffset, mItemOffset / 2)

    } else if (position % 2 == 1) { // right grid
        outRect.set(mItemOffset / 2, mItemOffset / 2, 0, mItemOffset / 2)

    } else {
        if (position % 2 == 0) { // left grid
            outRect.set(0, mItemOffset / 2, mItemOffset, mItemOffset)
        } else { // right grid
            outRect.set(mItemOffset / 2, mItemOffset / 2, 0, mItemOffset)
        }
    }
}

}

And to add this as a Item Decorator in RecyclerView, add below line:

/*spanCount is the number of grids, for instance, (2 = 2*2 grid, 3 = 3*3)*/
binding.rvActiveChallenges.addItemDecoration(GridItemOffsetDecoration(2, resources.getDimensionPixelSize(R.dimen._10dp)))
1

Here's my version. Based on Nicolay's answer but improved to work with a grid of three or more images & uses dp units for spacing. (His version doesn't give equal sized images/spacing with more than 2 images.)

NB: the logic to calculate spacing on each image is more complex than just dividing the spacing by two (half for each image) which most answers don't account for..

/**
 * Class to add INTERNAL SPACING to a grid of items. Only works for a grid with 3 columns or more.
 */
class PhotoSpaceDecoration extends RecyclerView.ItemDecoration {
    private final int spacingWidthPx;

    /**
     * Initialise with the with of the spacer in dp
     *
     * @param spacingWidthDp this will be divided between elements and applied as a space on each side
     *                       NB: for proper alignment this must be divisible by 2 and by the number of columns
     */
    public PhotoSpaceDecoration(Context context, int spacingWidthDp) {
        // Convert DP to pixels
        this.spacingWidthPx = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, spacingWidthDp,
                                                             context.getResources().getDisplayMetrics());
    }

    /**
     * @param index           a 0 indexed value of the current item
     * @param numberOfColumns
     * @return a 0 indexed Point with the x & y location of the item in the grid
     */
    private Point getItemXY(int index, int numberOfColumns) {
        int x = index % numberOfColumns;
        int y = index / numberOfColumns; // NB: integer division
        return new Point(x, y);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        final int position = parent.getChildAdapterPosition(view);
        final int columns = getTotalSpanCount(parent);
        final int rows = (int) Math.ceil(parent.getChildCount() / (double) columns); // NB: NOT integer division
        int spanSize = getItemSpanSize(parent, position);
        if (columns == spanSize) {
            return;
        }

        Point point = getItemXY(position, columns);
        int firstMargin = spacingWidthPx * (columns - 1) / columns;
        int secondMargin = spacingWidthPx - firstMargin;
        int middleMargin = spacingWidthPx / 2;

        if (point.x == 0) { // first column
            outRect.left = 0;
            outRect.right = firstMargin;
        } else if (point.x == 1) { // second column
            outRect.left = secondMargin;
            outRect.right = rows > 3 ? middleMargin : secondMargin;
        } else if (point.x - columns == -2) { // penultimate column
            outRect.left = rows > 3 ? middleMargin : secondMargin;
            outRect.right = secondMargin;
        } else if (point.x - columns == -1) { // last column
            outRect.left = firstMargin;
            outRect.right = 0;
        } else { // middle columns
            outRect.left = middleMargin;
            outRect.right = middleMargin;
        }

        if (point.y == 0) { // first row
            outRect.top = 0;
            outRect.bottom = firstMargin;
        } else if (point.y == 1) { // second row
            outRect.top = secondMargin;
            outRect.bottom = rows > 3 ? middleMargin : secondMargin;
        } else if (point.y - rows == -2) { // penultimate row
            outRect.top = rows > 3 ? middleMargin : secondMargin;
            outRect.bottom = secondMargin;
        } else if (point.y - rows == -1) { // last row
            outRect.top = firstMargin;
            outRect.bottom = 0;
        } else { // middle rows
            outRect.top = middleMargin;
            outRect.bottom = middleMargin;
        }
    }

    private int getTotalSpanCount(RecyclerView parent) {
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        return layoutManager instanceof GridLayoutManager ? ((GridLayoutManager) layoutManager).getSpanCount() : 1;
    }

    private int getItemSpanSize(RecyclerView parent, int position) {
        final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        return layoutManager instanceof GridLayoutManager ? ((GridLayoutManager) layoutManager).getSpanSizeLookup()
                                                                                               .getSpanSize(
                                                                                                       position) : 1;
    }
}

This is applied to the recycler view from the Activity.onCreate() as below

photosRecyclerView.addItemDecoration(new PhotoSpaceDecoration(this, 6));

Example: enter image description here

Inti
  • 3,443
  • 1
  • 29
  • 34
1

If you have header use this.

To hide the divider of header set skipHeaderDivider=false, otherwise set true.

class GridDividerItemDecoration : ItemDecoration() {

    var skipHeaderDivider = true

    private var divider: Drawable? = null

    private val bounds = Rect()

    private var spacing = 0

    fun setDrawable(drawable: Drawable) {
        divider = drawable
        divider?.intrinsicHeight?.let { spacing = it }
    }

    override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        canvas.save()
        val childCount = parent.childCount
        for (i in 0 until childCount) {
            val child = parent.getChildAt(i)
            parent.layoutManager?.getDecoratedBoundsWithMargins(child, bounds)

            val right: Int = bounds.right + child.translationX.roundToInt()
            val left: Int = bounds.left - child.translationX.roundToInt()
            val bottom: Int = bounds.bottom + child.translationY.roundToInt()
            val top: Int = bounds.top - child.translationY.roundToInt()

            divider?.setBounds(left, top, right, bottom)
            divider?.draw(canvas)
        }
        canvas.restore()
    }

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val gridLayoutManager = parent.layoutManager as? GridLayoutManager ?: return
        val position = gridLayoutManager.getPosition(view)
        if (position < 0) return
        val spanCount = gridLayoutManager.spanCount
        val positionalSpanSize = gridLayoutManager.spanSizeLookup.getSpanSize(position)

        if (skipHeaderDivider && positionalSpanSize == spanCount) return

        val itemCount = gridLayoutManager.itemCount

        val onBottom = position >= itemCount - spanCount
        var nextHeader = false

        run loop@{
            for (i in 1..spanCount) {
                val nextSpanSize = gridLayoutManager.spanSizeLookup.getSpanSize(position + i)
                if (nextSpanSize == spanCount) {
                    nextHeader = true
                    return@loop
                }
            }
        }

        outRect.top = spacing
        outRect.left = 0
        outRect.right = spacing
        outRect.bottom = if (nextHeader || onBottom) spacing else 0
    }
}
Yeldar Nurpeissov
  • 1,786
  • 14
  • 19
0

You can try my solution. this is flexible if you want list view or grid view.

    class GridItemOffsetDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)

        val totalSpanCount = getTotalSpanCount(parent)
        val spanSize = getItemSpanSize(parent, position)

        // if list view span == 1
        if (totalSpanCount == 1) {
            if (position == 0) {
                outRect.top = 10.dp
            } else {
                outRect.top = spacing
            }
            outRect.left = spacing
            outRect.right = spacing
            if (position == parent.adapter?.itemCount?.minus(1)) {
                outRect.bottom = spacing
            }
        } else { // if grid view span >= 2
            outRect.top = if (isInTheFirstRow(position, totalSpanCount)) 0 else spacing
            outRect.left = if (isFirstInRow(position, totalSpanCount, spanSize)) 0 else spacing / 2
            outRect.right = if (isLastInRow(position, totalSpanCount, spanSize)) 0 else spacing / 2
            outRect.bottom = 0

            when {
                position % 2 == 0 -> {
                    outRect.left = spacing
                }
                else -> {
                    outRect.right = spacing
                }
            }
            outRect.top = 10.dp
            if (position >= parent.adapter?.itemCount?.minus(2) ?: 0) { // minus(2) -> 2 is span count
                outRect.bottom = spacing
            }
        }
    }

    private fun isInTheFirstRow(position: Int, totalSpanCount: Int): Boolean =
        position < totalSpanCount

    private fun isFirstInRow(position: Int, totalSpanCount: Int, spanSize: Int): Boolean =
        if (totalSpanCount != spanSize) {
            position % totalSpanCount == 0
        } else true

    private fun isLastInRow(position: Int, totalSpanCount: Int, spanSize: Int): Boolean =
        isFirstInRow(position + 1, totalSpanCount, spanSize)

    private fun getTotalSpanCount(parent: RecyclerView): Int =
        (parent.layoutManager as? GridLayoutManager)?.spanCount ?: 1

    private fun getItemSpanSize(parent: RecyclerView, position: Int): Int =
        (parent.layoutManager as? GridLayoutManager)?.spanSizeLookup?.getSpanSize(position) ?: 1
}

NOTES : this is special for span count 2 case

  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-ask). – Community Sep 15 '21 at 19:19
  • Code-only answers are discouraged, so add some explanation to your answer. While that code may be fine, it is much more helpful if you also explain what it does, how it answers the question, and how it differs in approach from all the existing answers. – skomisa Sep 15 '21 at 19:22