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
20 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Get once a week my latest tutorials right in your inbox

Check your inbox to confirm your email