Search code examples
androidxmlandroid-layoutandroid-recyclerviewgradient

Android RecyclerView with gradient background


Is it possible to set the background of a recyclerview to a gradient so that the gradient spans the whole recyclerview rather than spanning the screen? In other words I want the background drawable height to be equal to the total height of all items, so that the background color changes when user scrolls down and that the end color of the gradient can only be reached when the user scrolls down to the last item.

<shape>
    <gradient
        android:startColor="@color/colorPrimary500"
        android:endColor="@color/colorSecondary50"
        android:angle="90"/>
</shape>

When I use this shape as the background of a recyclerview with height="wrap_content", I can scroll down but the gradient spans the screen height and it is static, the end color (yellow) is always at the bottom of the screen and the start color (blue) is always at the top: screenshot

One solution that I thought was setting the height of the recyclerview to the total height of its children but I don't know how it can be done.


Solution

  • There are a few ways to achieve that and to make that background scrollable.

    1. Move the background to a new view with the same height as the RecyclerView total height, and scroll it based on the RecyclerView current y scroll position.

    There are many downsides to this approach and it will not be easy to maintain as your app and codebase grows

    1. We could use a custom RecyclerView.ItemDecoration to manage that background and scroll it with the RecyclerView seamlessly.
    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.Rect
    import android.graphics.drawable.Drawable
    import android.view.View
    import androidx.recyclerview.widget.RecyclerView
    
    class GradientItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
    
        private var backgroundDrawable: Drawable? = null
    
        init {
            backgroundDrawable = context.getDrawable(R.drawable.background_gradient)
        }
    
        override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
            if (parent.layoutManager == null || backgroundDrawable == null) {
                return
            }
    
            val left = parent.paddingLeft
            val top = -parent.computeVerticalScrollOffset()
            val right = parent.width - parent.paddingRight
    
            val bottom = parent.computeVerticalScrollRange() + top
            backgroundDrawable?.setBounds(left, top, right, bottom)
            backgroundDrawable?.draw(canvas)
        }
    
        override fun getItemOffsets(
            outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
        ) = outRect.set(0, 0, 0, 0)
    
    }
    

    And then add it to RecyclerView.

    recyclerView = findViewById<RecyclerView>(R.id.recycler_view).apply {
        ....
    
        addItemDecoration(GradientItemDecoration(context))
    }
    

    In the above code, we loaded the gradient image in GradientItemDecoration and we update its bounds in onDraw as the user interacts with the RecyclerView.

    You can also extend from DividerItemDecoration instead if you want to still support dividers.


    Edit

    I created a Github repo showcasing this solution in action.