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 questions, please feel free to leave a comment below
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.
Thanks Artenes!
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.
RecyclerView’s animation for that slide up/down animation is kinda broken. It’s smooth when expand(slide down) the cell, but when collapse(slide up) the animation is not the same. With this method that I wrote, ValueAnimator handles the animations better.
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.
what is Arraylist
i am geetting error unresolved referrence
which library to import for this
Try to clean the cache. Go File > Invalidate Caches / Restart… and on the new window press Invalidate and Restart.
If you still have a problem, download the final project at the end of the tutorial and check what you did wrong.
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 !
You’re welcome! I’m glad I helped! 😁
how to make it possible to open only one at a time item?
My exact question bro
Hey, I just updated the tutorial with a new method on how to do it. Also, I added code to expand/collapse only one cell at a time
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!
Someone asked me the same on twitter months ago.
This will expand 1st cell by default when you open the app (change the position from 0 to your prefer cell position):
Thanks so much! I was close to the correct implementation of it!
Hmmm.. I think I can alter that to close the last opened cell when you click on any other cell..
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. 🙂
Hey!
Thanks for letting me know.
I just updated the tutorial showing a new way on how to do it.
Now it works perfectly! 👌