How to make Expandable RecyclerView using Kotlin

UPDATE [November 9th, 2020]: I changed the code/tutorial completely to fix that cell height bug. Also added code to show one cell at a time

Today, I’m going to show you how to make an expandable RecyclerView with smooth animation without using any 3rd party library.

Making Expandable RecyclerView

Go to your Activity’s XML file and paste the following code to add a RecyclerView

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/itemsrv" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" /> </RelativeLayout>
Code language: HTML, XML (xml)

And for the cell of the RecyclerView:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expand_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/black" android:orientation="vertical"> <TextView android:id="@+id/question_textview" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimary" android:gravity="start" android:padding="8dp" android:text="Question" android:textColor="@android:color/white" android:textSize="17sp" /> <TextView android:id="@+id/answer_textview" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/colorPrimaryDark" android:padding="15dp" android:text="Answer" android:textColor="@android:color/white" android:textSize="16sp" /> </LinearLayout>
Code language: HTML, XML (xml)

Set up your RecyclerView in your Activity (MainActivity.kt).

class MainActivity : AppCompatActivity() { private var itemsData = ArrayList<DataModel>() private var expandedSize = ArrayList<Int>() private lateinit var adapter: RVAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) adapter = RVAdapter(itemsData, expandedSize) val llm = LinearLayoutManager(this) itemsrv.setHasFixedSize(true) itemsrv.layoutManager = llm getData() itemsrv.adapter = adapter } private fun getData() { itemsData = ArrayList() itemsData = Data.items setCellSize() adapter.notifyDataSetChanged() adapter = RVAdapter(itemsData, expandedSize) } // Set the expanded view size to 0, because all expanded views are collapsed at the beginning private fun setCellSize() { expandedSize = ArrayList() for (i in 0 until itemsData.count()) { expandedSize.add(0) } } }
Code language: Kotlin (kotlin)

And for the RecyclerViewAdapter:

class RVAdapter(private val itemsCells: ArrayList<DataModel>, private val expandedSize: ArrayList<Int>) : RecyclerView.Adapter<RVAdapter.ViewHolder>() { private lateinit var context: Context class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { context = parent.context val v = LayoutInflater.from(parent.context).inflate(R.layout.recyclerview_cell, parent, false) return ViewHolder(v) } override fun getItemCount(): Int { return itemsCells.size } override fun onBindViewHolder(holder: ViewHolder, position: Int) { // Add data to cells holder.itemView.question_textview.text = itemsCells[position].question holder.itemView.answer_textview.text = itemsCells[position].answer // Set the height in answer TextView holder.itemView.answer_textview.layoutParams.height = expandedSize[position] // Expand/Collapse the answer TextView when you tap on the question TextView holder.itemView.question_textview.setOnClickListener { if (expandedSize[position] == 0) { // Calculate the height of the Answer Text val answerTextViewHeight = height(context, itemsCells[position].answer, Typeface.DEFAULT, 16, dp2px(15f, context)) changeViewSizeWithAnimation(holder.itemView.answer_textview, answerTextViewHeight, 300L) expandedSize[position] = answerTextViewHeight } else { changeViewSizeWithAnimation(holder.itemView.answer_textview, 0, 300L) expandedSize[position] = 0 } } } private fun changeViewSizeWithAnimation(view: View, viewSize: Int, duration: Long) { val startViewSize = view.measuredHeight val endViewSize: Int = if (viewSize < startViewSize) (viewSize) else (view.measuredHeight + viewSize) val valueAnimator = ValueAnimator.ofInt(startViewSize, endViewSize) valueAnimator.duration = duration valueAnimator.addUpdateListener { val animatedValue = valueAnimator.animatedValue as Int val layoutParams = view.layoutParams layoutParams.height = animatedValue view.layoutParams = layoutParams } valueAnimator.start() } private fun height(context: Context, text: String, typeface: Typeface?, textSize: Int, padding: Int): Int { val textView = TextView(context) textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize.toFloat()) textView.setPadding(padding, padding, padding, padding) textView.typeface = typeface textView.text = text val mMeasureSpecWidth = View.MeasureSpec.makeMeasureSpec(getDeviceWidth(context), View.MeasureSpec.AT_MOST) val mMeasureSpecHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) textView.measure(mMeasureSpecWidth, mMeasureSpecHeight) return textView.measuredHeight } private fun dp2px(dpValue: Float, context: Context): Int { val scale = context.resources.displayMetrics.density return (dpValue * scale + 0.5f).toInt() } private fun getDeviceWidth(context: Context): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val displayMetrics = DisplayMetrics() val display: Display? = context.display display?.getRealMetrics(displayMetrics) displayMetrics.widthPixels } else { val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val displayMetrics = DisplayMetrics() wm.defaultDisplay.getMetrics(displayMetrics) displayMetrics.widthPixels } } }
Code language: Kotlin (kotlin)

Extra: Expanding/Collapsing one at a time

If you want to expand/collapse only one cell at a time, you need to save the latest cell and hide it when you tap the next one.

Add the following code at the bottom of the setOnClickListener in the onBindViewHolder method:

// ... private var lastTappedCell: Int? = null override fun onBindViewHolder(holder: RVAdapter.ViewHolder, position: Int) { // ... holder.itemView.question_textview.setOnClickListener { // ... if (lastTappedCell != null) { expandedSize[lastTappedCell!!] = 0 notifyItemChanged(lastTappedCell!!) } lastTappedCell = position } }
Code language: Kotlin (kotlin)
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
18 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Artenes

Nice implementation. I’ve also made one some time ago, but it is very simples compared to yours: https://github.com/Artenes/recycler-view-expand-collapse.

Will study your code later to learn more on how to properly implement this expansion animation.

Ayush Jain

Nice read but just curious that why to do so much hard work because we can use one boolean variable in each item of list to make it’s expanded part to visible / invisible. And recyclerview gives pretty nice animation also for that i.e. slide up/down.

MobiAndy

hi,
Nice to read I need clarity if it is possible to add a list in the child (Recycler view inside another Recycler View) kindly make us a tutorial.

paresh

what is Arraylist
i am geetting error unresolved referrence
which library to import for this

paresh

sorry i mean i was asking about Arraylist of DataModel
what is DataModel ..
we can have Arraylist of String or Arraylist of Int etc so got confused at what is DataModel.
I have writen code from the above article .
but now clear after downloading your code its data class you declared,
now it working well.
Thank you John for this article & code !

Den

how to make it possible to open only one at a time item?

Ashraf Alabsi

My exact question bro

James

Hey John, Thanks so much for the great tutorial and implementation. I am having trouble figuring out how to make one of the recycler view items expanded by default. Unsure if that goes inside the List adapter or elsewhere.

Anyhelp would be greatly appreciated!

James

Thanks so much! I was close to the correct implementation of it!

Ashraf Alabsi

Hmmm.. I think I can alter that to close the last opened cell when you click on any other cell..

Yenne Lee

Hi. I want to let you know there is a bug. If you open an element that has many lines and just scroll up and down, and just open another element that has small lines, then second element height is not properly measured. (smaller or bigger than originally measured.) I mean, mExpandedViewHeight is… changed. Is there any advice? Thanks a lot. 🙂