I've always had this question in my earlier days as an Android Developer. Every time I had to develop a screen which required some form of expandable Recycler View, I'd be lost. However, I think I've come up with a very efficient solution to build this.
And I thought this would be a great topic for my First ever blog. So let's get into it.
I've created a very simple project to demonstrate this.
We start with a simple RecyclerView in MainActivity.kt
.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/list_item_view"
tools:itemCount="5"/>
The list_item_view.xml
will contain the views to display when the item is expanded and collapsed.
<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="wrap_content">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textColor="@color/black"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
tools:text="Item Number 1" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:contentDescription="@string/drop_down_arrow"
android:src="@drawable/ic_arrow_drop_down"
app:layout_constraintBottom_toBottomOf="@id/textView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/textView" />
<LinearLayout
android:id="@+id/expandableLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/do_you_want_to_check_this_item" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
The adapter is where the core logic exists. What we'll do here is store a list of all items that are expanded.
private var expandedItems: ArrayList<ItemData> = arrayListOf()
And every time an item is clicked upon, we simply add/remove the item from this list and call notifyItemChanged()
for that position.
binding.textView.setOnClickListener {
if (expandedItems.contains(item)) {
expandedItems.remove(item)
} else {
expandedItems.add(item)
}
notifyItemChanged(bindingAdapterPosition)
}
By doing this, we achieve a neat little expanding and collapsing animation that is automatically handled by Android. The notifyItemChanged()
is the secret formula.
Notice we do not change any UI elements here. All we do is simply change the state. That's because our bind() method is called and it takes care of displaying the appropriate views based on the state.
The ViewHolder class contains a bind()
method that sets up the item view.
fun bind(item: ItemData) {
val isExpanded = expandedItems.contains(item)
binding.textView.text = item.itemText
binding.checkbox.isChecked = item.isChecked
binding.imageView.setImageResource(
if (isExpanded) R.drawable.ic_arrow_drop_up
else R.drawable.ic_arrow_drop_down
)
binding.expandableLayout.isVisible = isExpanded
binding.textView.setOnClickListener {
if (expandedItems.contains(item)) {
expandedItems.remove(item)
} else {
expandedItems.add(item)
}
notifyItemChanged(bindingAdapterPosition)
}
binding.checkbox.setOnClickListener {
val isChecked = binding.checkbox.isChecked
itemList[bindingAdapterPosition].isChecked = isChecked
listener.onCheckChanged(bindingAdapterPosition, isChecked)
}
}
ItemData
is just a data class I created to store the state of each item in the RecyclerView.
data class ItemData(
var itemText: String,
var isChecked: Boolean = false
)
The adapter accepts an Interface object which allows for communication.
interface OnItemCheckChangedListener {
fun onCheckChanged(position: Int, isChecked: Boolean)
}
You can find the full source code in my Repository
Top comments (0)