Mehedi Hassan Piash [Sr. Software Engineer]

September 17, 2021

Expandable recyclerView in kotlin

September 17, 2021 Posted by Piash , No comments

 There are so many use cases for expandable recyclerView for example where there is a representation of category with subcategories or parent with a group of Childs. Let’s start coding step by step….

Image: Screen shot of expandable revcyclerView
  1. item_child.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textSize="12sp"
android:textColor="@color/parent_divider_color"
android:letterSpacing="0.2"
tools:text="@string/title" />
</LinearLayout>

2. item_parent

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">


<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="15sp"
android:gravity="center"
android:letterSpacing="0.2"
tools:text="@string/title" />
<View
android:layout_width="150dp"
android:layout_height="2dp"
android:layout_gravity="center"
android:background="@color/parent_divider_color"/>

<ImageView
android:id="@+id/down_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
app:srcCompat="@drawable/ic_baseline_arrow_down_24"/>

</LinearLayout>

3. fragment_expandable_recycler_view.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.fragment.ExpandableRecyclerViewFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/expandable_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</androidx.appcompat.widget.LinearLayoutCompat>

4. Items.kt

package com.piashcse.experiment.mvvm_hilt.model.expandable

interface Item {
fun getItemType(): Int
}

const val PARENT = 0
const val CHILD = 1

data class Parent(val parentItem: String) : Item {
var parent = parentItem
val childItems = ArrayList<Child>()
var isExpanded = false
var selectedChild: Child? = null

override fun getItemType() = PARENT
}

data class Child(
val parent: Parent,
val id: Int,
val title: String,
var price: Int = 0) : Item {
override fun getItemType() = CHILD
var isSelected: Boolean = false
}

data class Node(val id: Long, val parent: Node?) {

var isExpanded = false
val childList = ArrayList<Node>()
var nestingLevel = 0

init {
calculateNestingLevel()
}

private fun calculateNestingLevel() {
var current = parent
while (current != null) {
nestingLevel++
current = current.parent
}
}
}

5. ExpandableCategoryAdapter.kt

package com.piashcse.experiment.mvvm_hilt.ui.adapter

import com.piashcse.experiment.mvvm_hilt.model.expandable.Item
import com.piashcse.experiment.mvvm_hilt.model.expandable.Parent

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.piashcse.experiment.mvvm_hilt.R
import com.piashcse.experiment.mvvm_hilt.databinding.ItemChildBinding
import com.piashcse.experiment.mvvm_hilt.databinding.ItemParentBinding
import com.piashcse.experiment.mvvm_hilt.model.expandable.CHILD
import com.piashcse.experiment.mvvm_hilt.model.expandable.Child
import timber.log.Timber

class ExpandableCategoryAdapter(
private val context: Context,
private val itemList: ArrayList<Item>
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var currentOpenedParent: Parent? = null
var onItemClick: ((Child) -> Unit)? = null


override fun getItemViewType(position: Int): Int {
return itemList[position].getItemType()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
CHILD -> {
val bind =
ItemChildBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ChildViewHolder(
bind
)
}
else -> {
val bind =
ItemParentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ParentViewHolder(bind)
}
}
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
CHILD -> {
val childViewHolder = (holder as ChildViewHolder)
val childItem = itemList[position] as Child
childViewHolder.bind(childItem)
}
else -> {
val parentViewHolder = holder as ParentViewHolder
val parentItem = itemList[position] as Parent
parentViewHolder.bind(parentItem)
}
}
}

override fun getItemCount() = itemList.size

inner class ParentViewHolder(val binding: ItemParentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(parentItem: Parent) {
updateViewState(parentItem)
itemView.setOnClickListener {
val startPosition = adapterPosition + 1
val count = parentItem.childItems.size

if (parentItem.isExpanded) {
itemList.removeAll(parentItem.childItems)
notifyItemRangeRemoved(startPosition, count)
parentItem.isExpanded = false
currentOpenedParent = null

} else {
itemList.addAll(startPosition, parentItem.childItems)
notifyItemRangeInserted(startPosition, count)
parentItem.isExpanded = true

currentOpenedParent?.let { openParent ->
itemList.removeAll(openParent.childItems)
notifyItemRangeRemoved(
itemList.indexOf(openParent) + 1,
currentOpenedParent!!.childItems.size
)
currentOpenedParent?.isExpanded = false
notifyItemChanged(itemList.indexOf(openParent))
}

currentOpenedParent = parentItem
}
updateViewState(parentItem)
}
}

private fun updateViewState(parentItem: Parent) {
binding.apply {
if (parentItem.selectedChild != null) {
title.text = parentItem.selectedChild?.title
title.setTextColor(
ContextCompat.getColor(
context,
R.color.parent_divider_color
)
)
return
}
if (parentItem.isExpanded) {
downArrow.rotation = 180f
} else {
downArrow.rotation = 0f
}
title.text = parentItem.parent
}
}
}

inner class ChildViewHolder(val binding: ItemChildBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(childItem: Child) {
binding.apply {
title.text = childItem.title
if (childItem.isSelected) {
title.setTextColor(
ContextCompat.getColor(
context,
R.color.sender_bubble_text_color
)
)
} else {
title.setTextColor(
ContextCompat.getColor(
context,
R.color.parent_divider_color
)
)
}
}
itemView.setOnClickListener {
Timber.d("child item: ${childItem.isSelected}")
binding.apply {
if (childItem.isSelected) {
title.setTextColor(
ContextCompat.getColor(
context,
R.color.parent_divider_color
)
)
childItem.isSelected = false
} else {
title.setTextColor(
ContextCompat.getColor(
context,
R.color.sender_bubble_text_color
)
)
childItem.isSelected = true
}
}

onItemClick?.invoke(childItem)
}
}
}
}

6. ExpandableRecyclerViewFragment.kt

package com.piashcse.experiment.mvvm_hilt.ui.fragment

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.piashcse.experiment.mvvm_hilt.databinding.FragmentExpandableRecyclerViewBinding
import com.piashcse.experiment.mvvm_hilt.model.expandable.Child
import com.piashcse.experiment.mvvm_hilt.model.expandable.Item
import com.piashcse.experiment.mvvm_hilt.model.expandable.Parent
import com.piashcse.experiment.mvvm_hilt.ui.adapter.ExpandableCategoryAdapter


class ExpandableRecyclerViewFragment : Fragment() {
private var _binding: FragmentExpandableRecyclerViewBinding? = null
private val binding get() = requireNotNull(_binding) // or _binding!!
private lateinit var expandableAdapter: ExpandableCategoryAdapter
private val expandableItems = ArrayList<Item>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentExpandableRecyclerViewBinding.inflate(layoutInflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initView()
}

private fun initView() {
for (i in 1..5) {
val parent = Parent("Parent sequence $i")
val childItems = arrayListOf<Child>()
for (j in 1..5) {
childItems.add(Child(parent, j, "Child sequence $j"))
}
parent.childItems.addAll(childItems)
expandableItems.add(parent)
}
expandableAdapter = ExpandableCategoryAdapter(requireContext(), expandableItems)
binding.expandableRecycler.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = expandableAdapter
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

}

Ref: https://piashcse.medium.com/expandable-recyclerview-in-kotlin-876d14ecedcd

github: https://github.com/piashcse/blog_piashcse_code

0 comments:

Post a Comment