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.
Contents
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