Computer scienceMobileAndroidUser InterfaceGraphics

ItemDecoration

8 minutes read

RecyclerView is a key component in Android development because of its efficiency and flexibility. You can use it to display large data sets in a scrollable list, grid, or even custom layouts. While the crucial function of RecyclerView is to manage and display items efficiently, there are situations when you need to move past the basics to improve the user experience. This is when RecyclerView.ItemDecoration becomes necessary.

Basic ItemDecoration

Let's start by looking at the structure of the ItemDecoration class.

The ItemDecoration is an abstract class that has three methods:

  1. getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State): This method lets you set offsets around each item within a RecyclerView. These offsets help control the item's placement and spacing. The outRect signifies the space around the item. You can modify its properties, such as left, top, right, and bottom, to specify item spacing. The spacing will add to existing parent padding and item margins. The view parameter denotes the current item's View in the RecyclerView.

  2. onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State): This method lets you draw custom decorations on the RecyclerView's canvas. Everything this method draws is drawn before the item views and will be displayed beneath.

  3. onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State): Similar to onDraw, this method lets you design custom decorations on the RecyclerView's canvas. However, decorations drawn using onDrawOver will appear on top of the item views, unlike onDraw.

In all three methods, parent is the RecyclerView itself and state is the RecyclerView's state.

It's important to note that all these methods work with pixels, but in many cases, using dp is easier for us to ensure consistent decorations across different devices. To get the pixel density on the device, we'll do this:

// get amount of pixels(px) in 1dp
val density = resources.displayMetrics.density

Now, we can use this value to convert our dp into pixels for use in ItemDecoration:

val pixelsIn8Dp = (density * 8).toInt()

If you have to use many such dp values, it would be better to create a resource file with these values:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="icon_size">32dp</dimen>
    <dimen name="frame_width">10dp</dimen>
    <dimen name="offsets">8dp</dimen>
</resources>

Then you can just use the getDimensionPixelSize method:

resources.getDimensionPixelSize(R.dimen.icon_size)

Before we discuss our ItemDecoration, let's create a basic RecyclerView and an item with a button for it. Here's what we have:

Note that we have added a light blue background for our RecyclerView widget (which is not noticeable yet), a white background for the item with the button, with no padding or margins set.

Let's add offsets to differentiate items. To do this, we create the OffsetItemDecoration class, inheriting RecyclerView.ItemDecoration, and override the getItemOffsets method:

class OffsetItemDecoration(private val spacingInPx: Int) :
    RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.left = spacingInPx
        outRect.top = spacingInPx
        outRect.right = spacingInPx
        outRect.bottom = spacingInPx
    }
}

To add this decoration to our RecyclerView, we'll use the addItemDecoration method:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val recycler = findViewById<RecyclerView>(R.id.recycler_view)
    recycler.adapter = ItemRecyclerAdapter()
    recycler.layoutManager = LinearLayoutManager(this)
    recycler.addItemDecoration(
        OffsetItemDecoration(resources.getDimensionPixelSize(R.dimen.offsets))
    )
}

Here's the result:

When you modify the properties of outRect, you are specifying the amount of spacing between the item and its surrounding content or items. That's why we see our RecyclerView's light blue background.

You might notice that the offset between the first and second items (and all subsequent items) is bigger than the left and right offsets of each item.

This occurs because when you add an ItemDecoration to a RecyclerView, the getItemOffsets method is called for each item in the order they are added to the layout. The RecyclerView's layout manager considers the offsets you've defined for the current item. This spacing is used to decide where to place the next item. So we have an offset equal to 8dp + 8dp, rather than just 8dp.

You can try to correct it yourself or wait for the next section of the topic where we show the solution.

Now it's time to tackle the onDraw() and onDrawOver() methods.

For that, we'll create another class with decorations:

class IconItemDecoration(
    private val iconDrawable: Drawable, private val iconSizeInPx: Int
) : RecyclerView.ItemDecoration() {

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

        // parent.childCount returns the number of items displayed on the screen
        for (i in 0 until parent.childCount) { 
            
            // parent.getChildAt(index: Int) returns the view of a specific item 
            val view = parent.getChildAt(i)

            val left = view.left
            val top = view.top
            val right = left + iconSizeInPx
            val bottom = top + iconSizeInPx
            /* try to replace bottom for the following code and 
            you'll get an interesting result*/
            // val bottom = (child.bottom - top) / 2 + iconSizeInPx / 2

            iconDrawable.setBounds(left, top, right, bottom)
            iconDrawable.draw(c)
        }
    }
}

In the above code, we iterate through each child view in the RecyclerView and draw the iconDrawable that is passed as a parameter to the constructor at a specific position relative to that child view. Since the onDraw method is called whenever the RecyclerView is redrawn, the icons move along with the items in the RecyclerView.

Let's add this decoration to our RecyclerView and see the result:

And there's no change...

You might have guessed the problem – the onDraw() method draws its content before the items are drawn, and since our icons are situated exactly in the items' corner, we can't see them. The solution is to change onDraw() to onDrawOver():

And now it works!

We've worked through the ItemDecoration methods. But let's look at one more example:

class FrameItemDecoration(private val frameWidthInPx: Int): RecyclerView.ItemDecoration() {

    private val framePaint = Paint().apply {
        this.isAntiAlias = true
        this.style = Paint.Style.STROKE
        this.strokeWidth = frameWidthInPx.toFloat()
        this.color = Color.parseColor("#CCE5FF")
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

        for (i in 0 until parent.childCount) {
            val view = parent.getChildAt(i)

            /* rect top,left,right,bottom coordinates are in the middle 
            of the frame border, so it should start to draw from 
            view.left + half of the desired width */

            val left = (view.left + frameWidthInPx / 2).toFloat()
            val top = (view.top + frameWidthInPx / 2).toFloat()
            val bottom = (view.bottom - frameWidthInPx / 2).toFloat()
            val right = (view.right - frameWidthInPx / 2).toFloat()

            // the fifth (rx) parameter is the outer radius of the corner
            // the sixth (ry) parameter is the inner radius of the corner
            c.drawRoundRect(left, top, right, bottom, 32f, 32f, framePaint)
        }
    }
}

Adding this decoration (without the previous ones) will result in:

This example demonstrates how flexible ItemDecoration is. It allows you to draw almost anything you want.

As you might have noticed, when positioning our decorations, we depend on the item's position, calculating it using view.left/top/right/bottom. But what if our items have some animation? Will the mentioned method provide the correct position in these cases? Unfortunately no. But, there is a way to obtain the necessary data. Let's see how we can do that.

Let's consider a few properties that are responsible for a view's position:

view.left:

  • view.left is the visual x-coordinate of the view's left edge relative to its parent in pixels.

  • It does not include any translation applied to the view.

view.x:

  • view.x is the visual x-coordinate of the top-left corner of the view relative to its parent.

  • It includes any translation applied to the view. If the view has been translated horizontally, view.x will reflect the updated position.

view.translationX:

  • view.translationX is the horizontal translation of the view from its original position.

  • It indicates how far the view has moved horizontally from its initial layout position.

Similarly, view.top gives the original vertical position, view.y reflects the current visual position, and view.translationY shows the vertical translation of a view.

The choice of which to use depends on whether you want your drawing to be based on the original layout positions or the current visual positions of items in the RecyclerView.

For most part, we recommend using .x and .y because RecyclerView almost always has some animations that may affect your decorations' positioning and lead to undesirable results.

ItemDecoration advantages

The RecyclerView.ItemDecoration tool helps you customize the appearance of a RecyclerView without changing the underlying data. It provides several key advantages compared to adding decorations through XML layout:

  • Separating decoration from logic

The RecyclerView.ItemDecoration allows you to separate the decoration of items from the logic of your RecyclerView. This adherence to the principle of separation of concerns is essential for effective software design. It lets you distinguish between the part of the code that decides how items appear (the decoration) from the part that determines how items function (the logic). As a result, your code becomes more modular and easier to comprehend and test.

  • Centralized and simplified decoration management

An essential advantage of using ItemDecoration is that it provides a centralized point for managing and applying decorations to your RecyclerView items. Instead of weaving decoration logic into each individual item layout or RecyclerView adapter, it acts as a hub for complete decoration control. This proves particularly useful when you have various types of decorations spread across different items in your RecyclerView. By having a single management point within your code, maintaining and updating becomes significantly easier.

  • Reusability across item types and RecyclerViews

You can reuse the ItemDecoration. After creating an ItemDecoration, you can apply it to different item types within the same RecyclerView, or even across different RecyclerViews. This potential for reusability saves you time and effort. It's especially beneficial when dealing with several RecyclerViews with similar decoration requirements or complex layouts featuring multiple types of items or RecyclerViews.

  • Dynamic Customization

You can also dynamically customize decorations with ItemDecoration. With a few lines of code, you can alter your items' appearance based on various factors, like their position in the RecyclerView or the data they hold. This feature offers multiple possibilities for enhancing the user experience.

However, note that ItemDecoration is not interactive and does not include accessibility features. Decorations are not clickable, do not respond to user input, and limit drawing visuals without the ability to incorporate useful text. This is a key point, as decorations intend to offer visual enhancement without disturbing the items' core functionality.

Understanding the fundamental benefits of using ItemDecoration will better prepare you to utilize its capabilities depending on the use case. In the following section, you will see more code examples and practical applications of ItemDecoration. This knowledge will equip you to fully exploit this tool to improve the visual appeal of your RecyclerViews.

Advanced ItemDecoration

In this section, you will not encounter ready-to-use implementations of ItemDecoration. Instead, we will focus on introducing different methods and strategies for working with item decorations and guiding you on how to create any type of decoration you wish. Let's begin.

First, let's adjust our code from the prior section, so it sets offsets between items:

class OffsetItemDecoration(private val spacingInPx: Int) :
    RecyclerView.ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        outRect.left = spacingInPx
        outRect.right = spacingInPx
        outRect.bottom = spacingInPx

        val position = parent.getChildViewHolder(view).layoutPosition

        if (position == 0) {
            outRect.top = spacingInPx
        }
    }
}

As you can see, we added conditions that determine whether to add a bottom (or top) offset for a particular item on the screen. This prevented us from adding extra offsets between adjacent elements.

This straightforward example helps us understand that creating high-quality decorations requires having data about our RecyclerView and the items within it. There are many properties and methods available for this. Here are some that are frequently used when working with decorations:

  • parent.childCount: This property shows the number of child views (items) currently attached to the RecyclerView. It tells you how many items are currently visible on the screen.

  • parent.getChildViewHolder(child: View): This method returns the ViewHolder associated with a child view in the RecyclerView. You can use it to access the ViewHolder of a specific item view, which might be useful for executing actions or accessing data related to that item.

  • parent.getChildViewHolder(view) is MyFirstViewHolder: This type-checking expression lets you identify the specific type of ViewHolder associated with a child view. This way, you can understand what kind of item it is. This approach is particularly useful in RecyclerViews when you have different item layouts within the same RecyclerView.

  • parent.getChildViewHolder(view).layoutPosition: This method returns the layout position of the child view within the RecyclerView, considering the current visual state, ongoing animations, or changes. With this, you can identify the position of a specific item view in the RecyclerView's layout.

  • parent.itemDecorationCount: This property returns the number of ItemDecoration objects currently attached to the RecyclerView. It helps you ascertain how many ItemDecoration instances are affecting the RecyclerView's appearance.

  • parent.getItemDecorationAt(index: Int): This method allows you to retrieve the ItemDecoration at a specific index. You can use it to access or modify the ItemDecoration at a particular position in the decoration stack.

  • parent.removeItemDecorationAt(index: Int): This method removes the ItemDecoration at a specified index from the RecyclerView. It comes in handy when you want to dynamically add or remove decorations at runtime.

  • parent.invalidateItemDecorations(): This method invalidates all the item decorations applied to the RecyclerView. It triggers a re-layout and redraw of the decorations, which can be helpful if you need to refresh the decorations for any reason.

As you may know, in a RecyclerView, each visible item on the screen is supported by data from your dataset. When an item scrolls off the screen, the RecyclerView doesn't destroy its view but disassociates this view from any data in your dataset. The RecyclerView then reuses this view for new items that scroll onscreen.

Since ItemDecorations are associated with views, they don't persist when views are recycled. When a view is reused for a different item, the decoration is lost.

This means that you can't rely on the decoration to transport data or state from one view to another, or even to store or manipulate data associated with items in the RecyclerView.

Now, let's practice some more. To do this, we will modify our FrameItemDecoration to create a universal frame for every three items. To achieve this, let's make a large frame with its top border aligned with the first item and the bottom border with the third:

class FrameItemDecoration(private val frameWidthInPx: Int) :
    RecyclerView.ItemDecoration() {

    private val framePaint = Paint().apply {
        this.isAntiAlias = true
        this.style = Paint.Style.STROKE
        this.strokeWidth = frameWidthInPx.toFloat()
        this.color = Color.parseColor("#CCE5FF")
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

        val diff = (frameWidthInPx / 2).toFloat()

        for (i in 0 until parent.childCount) {
            val view = parent.getChildAt(i)
            val position = parent.getChildViewHolder(view).layoutPosition

            if ((position + 1) % 3 == 1) {

                val nextView = parent.getChildAt(i + 2) ?: continue
                val left = (view.left + diff)
                val top = (view.top + diff)
                val right = (view.right - diff)
                val bottom = (nextView.bottom - diff)

                c.drawRoundRect(left, top, right, bottom, 32f, 32f, framePaint)
            }
        }
    }
}

Here's what we got:

In the image above, if you consider items 16 and 17, we'd expect to see a frame, but it's missing. This is because the third element of the trio has not yet appeared on the screen, hence we cannot find its coordinates to draw the frame, so the frame becomes visible only with a delay, leading to a lower user experience.

A straightforward solution for this issue is to draw a separate frame element for each item within the trio:

class FrameItemDecoration(private val frameWidthInPx: Int) :
    RecyclerView.ItemDecoration() {

    private val framePaint = Paint().apply {
        this.isAntiAlias = true
        this.style = Paint.Style.STROKE
        this.strokeWidth = frameWidthInPx.toFloat()
        this.color = Color.parseColor("#CCE5FF")
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {

        for (i in 0 until parent.childCount) {
            val view = parent.getChildAt(i)
            val diff = (frameWidthInPx / 2).toFloat()
            val cornerR = 32f

            val left = (view.left + diff)
            val top = (view.top + diff)
            val bottom = (view.bottom - diff)
            val right = (view.right - diff)

            val index = parent.getChildViewHolder(view).layoutPosition
            when ((index + 1) % 3) {
                0-> {
                    c.drawLine(left + cornerR, bottom, right - cornerR, bottom, framePaint)

                    // Draw an arc to create rounded corners for the frame.
                    c.drawArc(left, bottom - 2 * cornerR, left + 2 * cornerR, bottom,
                        90f, 90f, false, framePaint)
                    c.drawArc(right - 2 * cornerR, bottom - 2 * cornerR, right, bottom,
                        0f, 90f, false, framePaint)
                    c.drawLine(left, top, left, bottom - cornerR, framePaint)
                    c.drawLine(right, top, right, bottom - cornerR, framePaint)
                }
                1-> {
                    c.drawLine(left + cornerR, top, right - cornerR, top, framePaint)
                    c.drawArc(left, top, left + 2 * cornerR, top + 2 * cornerR,
                        180f, 90f, false, framePaint)
                    c.drawArc(right - 2 * cornerR, top, right, top + 2 * cornerR,
                        270f, 90f, false, framePaint)
                    c.drawLine(left, top + cornerR, left, bottom, framePaint)
                    c.drawLine(right, top + cornerR, right, bottom, framePaint)

                }
                2-> {
                    c.drawLine(left, top - frameWidthInPx, left, bottom + frameWidthInPx, framePaint)
                    c.drawLine(right, top - frameWidthInPx, right, bottom + frameWidthInPx, framePaint)
                }
            }
        }
    }
}

We got what we aimed for:

Now everything works, and we can still see part of the frame, even though the lower item has not appeared yet. However, we can attain the same result by slightly refining the code that didn't work earlier:

class FrameItemDecoration(private val frameWidthInPx: Int) :
    RecyclerView.ItemDecoration() {

    private val framePaint = Paint().apply {
        this.isAntiAlias = true
        this.style = Paint.Style.STROKE
        this.strokeWidth = frameWidthInPx.toFloat()
        this.color = Color.parseColor("#CCE5FF")
    }

    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        if (parent.childCount >= 1) { // check is there any item in recyclerView
            val frameHeight = parent.getChildAt(0).height * 3 - frameWidthInPx
            val diff = (frameWidthInPx / 2).toFloat()

            for (i in 0 until parent.childCount) {
                val view = parent.getChildAt(i)
                val position = parent.getChildViewHolder(view).layoutPosition
                val left = (view.left + diff)
                val right = (view.right - diff)
                
                if ((position + 1) % 3 == 1) {
                    val top = (view.top + diff)
                    val bottom = (top + frameHeight)
                    c.drawRoundRect(left, top, right, bottom, 32f, 32f, framePaint)
                } else if ((position + 1) % 3 == 0) {
                    val bottom = (view.bottom - diff)
                    val top = (bottom - frameHeight)
                    c.drawRoundRect(left, top, right, bottom, 32f, 32f, framePaint)
                }
            }
        }
    }
}

This code delivers the desired result and is more understandable than the prior one. Using this frame as a reference, it's clear that drawing with Canvas is not complex. It allows us to accomplish exciting results, but it requires attention and accuracy, especially when working on a dynamic RecyclerView. Therefore, if you want to be able to create fascinating decorations, take the time to study the Canvas methods and practice their application.

Although Canvas is suitable for dynamic drawings, creating complicated decorative elements in XML layout might be more efficient in terms of development time. XML offers a declarative way to express UI, which can be beneficial for certain static or less dynamic elements, saving time compared to manually drawing with Canvas. When deciding between XML layout and Canvas, consider the complexity and dynamism of your decoration.

This shows us that to work effectively with ItemDecoration, you should prioritize practicing how to draw these decorations.

Conclusion

ItemDecoration is a powerful tool for customizing the RecyclerView in Android. It gives you exact control over item layout and decoration drawing. You have learned getItemOffsets, onDraw, and onDrawOver methods, along with the merits of using ItemDecoration. Keep in mind, mastering ItemDecoration depends on regular practice and exploration. Feel free to explore different designs and techniques to unlock its full potential.

6 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo