Search code examples
androidandroid-recyclerviewgridlayoutmanager

Rows in GridLayoutManager expand but do not shrink when item content changes


I have a RecyclerView with a GridLayoutManager. Every item has a button and a invisible content. When the user presses the button, the content switches its visibility.

When the content is invisible and the user press the button for the first time, the item expands, and so do the other items on the same row

Status before pressing any button

Status after pressing a button

The problem arises after the user presses again the button: the row doesn't shrink

The row doesn't shrink

The same happens for every row

Status after pressing the button again

The code I'm using is quite simple... A very basic adapter

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/box"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:layout_margin="8dp"
    android:layout_weight="1"
    android:background="#00ff00"
    android:orientation="vertical"
    android:padding="8dp">

    <com.google.android.material.button.MaterialButton
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:text="Press me"
        android:visibility="visible" />

    <View
        android:id="@+id/hidden_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:background="#0000ff"
        android:visibility="gone" />

</LinearLayout>
class TestAdapter : RecyclerView.Adapter<ViewHolder>() {

    val items = arrayOf(false, false, false, false)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            ItemTestBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    override fun getItemCount(): Int {
        return items.size
    }

     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemBinding.hiddenView.isVisible = items[position]

        holder.itemBinding.button.setOnClickListener {
            items[position] = !items[position]
            notifyItemChanged(position)
        }

        return
    }
}

open class ViewHolder internal constructor(val itemBinding: ItemTestBinding) :
    RecyclerView.ViewHolder(itemBinding.root)

In a very basic layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff0000"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
        app:layout_constraintTop_toTopOf="parent"
        app:spanCount="2"
        tools:itemCount="4"
        tools:listitem="@layout/item_test" />

    <View
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="#ffff00"
        app:layout_constraintTop_toBottomOf="@id/recycler"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.recycler.adapter = TestAdapter()
    }
}

I'm expecting to have the row shrink to the lower item

Please, note that when using a LinearLayout or a GridLayoutManager with a spanCount = 1, the behaviour is correct:

Start

Press

Press again

The problem seems to be related to this one:

Items height in GridLayoutManager after dynamic action but that solution is not working here

This is a minimal project that replicates the problem:

https://mega.nz/file/R7IgmSwD#mwy28aMvMTagfkzSNjArIHANI5cUskG8vN7ZrVp-u94


Solution

  • After the hint of @RyanM and @Biscuit, I found a solution (or a workaround, I'm not sure about how to consider that).

    When I update the item I do not just need to notify the recycler about the fact that I updated that single item, but also that all the items on the same row are updated: when an item is expanded, all the other items on the row change their height. So, calling

    notifyItemChanged(position)

    will make the layout manager recalculate that item height, but all the other items on the row will keep the "expanded" height, so the row won't shrink.

    Calling

    notifyDataSetChanged()

    will force the GridLayoutManager to recalculate all the view sizes and will make the row shrink as expected.

    This is the working code for onBindViewHolder

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemBinding.hiddenView.isVisible = items[position]
    
        holder.itemBinding.button.setOnClickListener {
            items[position] = !items[position]
    
            // Just a tiny optimization, that's needed only when shrinking
            if (!items[position])
                notifyDataSetChanged()
        }
    
        return
    }