How to make Expandable RecyclerView using Kotlin

Last updated on: December 1, 2019

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