Decorating RecyclerView (with GridLayoutManager) to display divider between items

后端 未结 9 1440
旧时难觅i
旧时难觅i 2020-12-07 09:44

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

\"enter

相关标签:
9条回答
  • 2020-12-07 09:49

    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)))
    
    0 讨论(0)
  • 2020-12-07 09:56

    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

    0 讨论(0)
  • 2020-12-07 10:00

    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:

    0 讨论(0)
  • 2020-12-07 10:02

    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
    
        }
    }
    
    0 讨论(0)
  • 2020-12-07 10:05

    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:

    0 讨论(0)
  • 2020-12-07 10:05

    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
        }
    }
    
    0 讨论(0)
提交回复
热议问题