fbpx
How to make Expandable RecyclerView using Kotlin

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>

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>

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)
        }
    }
}

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
        }
    }

}

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
    }
}
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. 🙂