在開發 Android 時,一般會用 RecyclerView 來繪製列表。當列表中的每一列長得不太一樣時,我們就需要對每一種列建立一個 ViewHolder。讓我們看看要如何讓 RecyclerView 使用多種 ViewHolder。
Table of Contents
專案建立
建立一個新的 Empty Activity 專案。之後我們會在這 MainActivity 上,用 RecyclerView 繪製一個列表。列表裡面會有三種不同的列。
了解 RecyclerView
我們假設你已經知道如何使用 RecyclerView 來繪製單一 ViewHolder 的列表。如果你還沒使用過 RecyclerView,或是不太熟悉它的話,可以先參考以下的文章。
RecyclerView.Adapter
新增 RowData,其程式碼如下。它是所有種列的通用 interface。裡面的 method 等等會和 adapter 一起講解。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.view.View import androidx.recyclerview.widget.RecyclerView interface RowData { val layout: Int fun onCreateViewHolder(view: View): RecyclerView.ViewHolder fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) }
新增 RowAdapter,其程式碼如下。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView class RowAdapter(private val list: List<RowData>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val rowData = list.find { it.layout == viewType }!! val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(viewType, parent, false) return rowData.onCreateViewHolder(view) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val rowData = list[position] rowData.onBindViewHolder(holder) } override fun getItemViewType(position: Int): Int { return list[position].layout } override fun getItemCount(): Int { return list.size } }
RowAdapter 繼承 RecyclerView.Adapter,並且接收一個 List<RowData>。
- getItemCount():傳回列的總數。
- getItemViewType():ItemViewType 指的是當前列(item view)的 type。所以說,有多少種 item view,就會有多少種 type。這邊回傳 layout,因為剛好 layout 的 type 是 Int,而且每一種 item view 都有自己的 layout。
- onCreateViewHolder():在這 method 裡,viewType 其實就是 layout。找出使用相同 layout 的 RowData,呼叫 RowData.onCreateViewHolder(),在那裡面會建立 ViewHolder。
- onBindViewHolder():在這 method 裡,呼叫 RowData.onBindViewHolder() 來綁定資料。
建立多種 ViewHolder
接下來,我們來建立三種 ViewHolder 來熟悉一下如何使用 RowData 來建立各種不同的 ViewHolder。
TextRowData
新增 row_text.xml。它包含兩個 TextView,一個顯示 title,一個顯示值。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/titleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/valueTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
新增 TextRowData 並繼承 RowData。TextRowData 相當地簡單,所以我們就不多加解釋了。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView class TextRowData(private val title: String, private val value: String) : RowData { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val titleTextView: TextView = itemView.findViewById(R.id.titleTextView) val valueTextView: TextView = itemView.findViewById(R.id.valueTextView) } override val layout = R.layout.row_text override fun onCreateViewHolder(view: View) = ViewHolder(view) override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) { viewHolder as ViewHolder viewHolder.titleTextView.text = title viewHolder.valueTextView.text = value } }
EditRowData
新增 row_edit.xml。它顯示一個 title,和一個文字輸入框。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/titleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/valueEditText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:importantForAutofill="no" android:inputType="textPersonName" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="LabelFor" /> </androidx.constraintlayout.widget.ConstraintLayout>
新增 EditRowData。它和 TextRowData 差不多,但是多了一個 doAfterTextChanged
的 callback。當輸入框裡面的文字被更動後,callback 就會被呼叫。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.text.Editable import android.view.View import android.widget.EditText import android.widget.TextView import androidx.core.widget.doAfterTextChanged import androidx.recyclerview.widget.RecyclerView class EditRowData( private val title: String, private val value: String, private val doAfterTextChanged: (text: String) -> Unit ) : RowData { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val titleTextView: TextView = itemView.findViewById(R.id.titleTextView) val valueEditText: EditText = itemView.findViewById(R.id.valueEditText) } override val layout = R.layout.row_edit override fun onCreateViewHolder(view: View) = ViewHolder(view) override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) { viewHolder as ViewHolder viewHolder.titleTextView.text = title viewHolder.valueEditText.text = Editable.Factory.getInstance().newEditable(value) viewHolder.valueEditText.doAfterTextChanged { it.toString().let(doAfterTextChanged) } } }
SwitchRowData
新增 row_switch.xml。它顯示一個 title,和一個開關。
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/titleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.appcompat.widget.SwitchCompat android:id="@+id/valueSwitch" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
新增 SwitchRowData,它和 EditRowData 差不多。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.view.View import android.widget.TextView import androidx.appcompat.widget.SwitchCompat import androidx.recyclerview.widget.RecyclerView class SwitchRowData( private val title: String, private val value: Boolean, private val onCheckedChange: (Boolean) -> Unit ) : RowData { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val titleTextView: TextView = itemView.findViewById(R.id.titleTextView) val valueSwitch: SwitchCompat = itemView.findViewById(R.id.valueSwitch) } override val layout = R.layout.row_switch override fun onCreateViewHolder(view: View) = ViewHolder(view) override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder) { viewHolder as ViewHolder viewHolder.titleTextView.text = title viewHolder.valueSwitch.isChecked = value viewHolder.valueSwitch.setOnCheckedChangeListener { buttonView, isChecked -> onCheckedChange(isChecked) } } }
在 MainActivity 中使用 RowAdapter
最後,我們在 MainActivity 上用 RecyclerView 繪製一個列表。
新增 activity_main.xml,裡面顯示一個 RecyclerView。
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout 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/mainRecyclerView" android:layout_width="match_parent" android:layout_height="match_parent" /> </androidx.coordinatorlayout.widget.CoordinatorLayout>
在 MainActivity 裡,新增 RowAdapter,並新增三個列。
package com.waynestalk.androidrecyclerviewmultipleitemsexample import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val recyclerView = findViewById<RecyclerView>(R.id.mainRecyclerView) recyclerView.adapter = RowAdapter( listOf( TextRowData("Name", "Wayne's Take"), EditRowData("Phone Number", "12345678") { println("Phone number: $it") }, SwitchRowData("Registered", false) { println("Registered: $it") } ) ) recyclerView.layoutManager = LinearLayoutManager(this) } }
這樣就大功完成了!
結論
本章中最主要的就是 RowData。它將每個列相關的邏輯與程式碼獨立出來,並都包含在一個 class 裡面。這樣不但降低了 RowAdapter 的複雜度,每個檔案的程式碼也相當地短。