oligazar
6/3/2017 - 9:19 AM

RecyclerView ItemDecorations

  1. DividerItemDecoration supports both vertical and horizontal lists. Takes optional res id as a divider drawable as parameters

  2. GridItemDecorator respects margins between grid items in a RecyclerView backed by GridLayoutManager

  3. item_devider.xml

  4. HorizontalSpaceItemDecoration and VerticalSpaceItemDecoration

  5. VerticalSpaceItemDeoration

  6. ItemOffsetDecoration

class ItemOffsetDecoration(private val spacingDp: Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
                                state: RecyclerView.State) {
        val context = parent.context
        val spacingPx = context.dp(spacingDp.toFloat())

        super.getItemOffsets(outRect, view, parent, state)
        outRect.set(spacingPx, spacingPx, spacingPx, spacingPx)
    }
    private fun Context.dp(dp: Float) = Math.round(dp * resources.displayMetrics.density)
}
class GridItemDecorator(private val gridSize: Int,
                        private val spacingDp: Int = 0,
                        private val top: Int = 0,
                        private val sides: Boolean = false) : RecyclerView.ItemDecoration() {

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

        val context = parent.context
        val spacingPx = context.dp(spacingDp.toFloat())

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

        outRect.top = if (itemPosition < gridSize) context.dp(top) else  bit * gridSize

        val rowPosition = itemPosition % gridSize
        if (sides) {
            outRect.left = (gridSize * bit) - (rowPosition * bit)
            outRect.right = (rowPosition + 1) * bit
        } else {
            outRect.left = rowPosition * bit
            outRect.right = (gridSize - rowPosition - 1) * bit
        }
    }

    private fun Context.dp(dp: Int) = Math.round(dp * resources.displayMetrics.density)
    private fun Context.dp(dp: Float) = Math.round(dp * resources.displayMetrics.density).toFloat()
}

// Add bottom margin through layout
 <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:background="@color/colorGrayLight"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        
        android:paddingBottom="8dp"
        android:clipToPadding="false"
        
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

// Supports both vertical and horizontal lists. 
// Takes optional res id as a divider drawable
class MyDividerItemDecoration(context: Context, resId: Int? = null, orientation: Int = VERTICAL_LIST) : RecyclerView.ItemDecoration() {

    private val divider: Drawable
    private var orientation: Int = 0

    init {
        if (resId == null) {
            val a = context.obtainStyledAttributes(ATTRS)
            divider = a.getDrawable(0)
            a.recycle()
        } else {
            divider = ContextCompat.getDrawable(context, resId)
                    ?: throw IllegalArgumentException("Drawable with id $resId doesn't exist!")
        }
        setOrientation(orientation)
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        if (orientation == VERTICAL_LIST) {
            drawVertical(c, parent)
        } else {
            drawHorizontal(c, parent)
        }
    }

    private fun drawVertical(c: Canvas, parent: RecyclerView) {
        val left = parent.paddingLeft
        val right = parent.width - parent.paddingRight

        val childCount = parent.childCount
        for (i in 0 until childCount - 1) {
            val child = parent.getChildAt(i)
            val params = child
                    .layoutParams as RecyclerView.LayoutParams
            val top = child.bottom + params.bottomMargin
            val bottom = top + divider.intrinsicHeight
            divider.setBounds(left, top, right, bottom)
            divider.draw(c)
        }
    }

    private fun drawHorizontal(c: Canvas, parent: RecyclerView) {
        val top = parent.paddingTop
        val bottom = parent.height - parent.paddingBottom

        val childCount = parent.childCount
        for (i in 0 until childCount - 1) {
            val child = parent.getChildAt(i)
            val params = child
                    .layoutParams as RecyclerView.LayoutParams
            val left = child.right + params.rightMargin
            val right = left + divider.intrinsicHeight
            divider.setBounds(left, top, right, bottom)
            divider.draw(c)
        }
    }

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        if (orientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, divider.intrinsicHeight)
        } else {
            outRect.set(0, 0, divider.intrinsicWidth, 0)
        }
    }

    private fun setOrientation(orientation: Int) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw IllegalArgumentException("invalid orientation")
        }
        this.orientation = orientation
    }

    companion object {
        private val ATTRS = intArrayOf(android.R.attr.listDivider)
        val HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL
        val VERTICAL_LIST = LinearLayoutManager.VERTICAL
    }
}
class VerticalSpaceItemDecoration(resources: Resources, 
                                  vSpaceDip: Int, 
                                  headerDip: Int = -1, 
                                  footerDip: Int = -1, 
                                  val isDip: Boolean = true) : RecyclerView.ItemDecoration() {

    private val header = if (headerDip == -1) vSpaceDip else headerDip
    private val footer = if (footerDip == -1) vSpaceDip else footerDip
    private val mVerticalSpaceHeight = if (isDip) dp(vSpaceDip, resources) else vSpaceDip
    private val mHeaderHeight = if (isDip) dp(header, resources) else header
    private val mFooterHeight = if (isDip) dp(footer, resources) else footer

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
        when {
            parent.getChildAdapterPosition(view) == 0 -> {
                // vertical spaces for the first element
                outRect.top = mHeaderHeight
                outRect.bottom = mVerticalSpaceHeight
                // vertical spaces for the last element
            }
            parent.getChildAdapterPosition(view) == parent.adapter.itemCount - 1 -> outRect.bottom = mFooterHeight
            else -> // vertical spaces for other elements
                outRect.bottom = mVerticalSpaceHeight
        }
    }
}
class HorizontalItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        val spacePx = dp(space, view.context.resources)
        outRect.right = spacePx

        // Add top margin only for the first item to avoid double space between items
        if (parent.getChildLayoutPosition(view) == 0)
            outRect.left = spacePx
    }
}

fun dp(dp: Int, resources: Resources) = Math.round(dp * resources.displayMetrics.density)
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">
    <size android:height="1dp" />
    <solid android:color="@color/colorGray" />
</shape>