我們上一篇介紹了 RecyclerView
的基本使用,但是實務上可能會有更進階的需求,比如說我們可以要取得點擊某一個子 View
的事件,或是我們每個子 View
中間都要加上一條線,或是我們可能有些子 View
要長不一樣。又或者是我們需要動態增加或修改整個 RecyclerView
的資料。
我們之前介紹過 View
可以設 OnClickListener
來取得點擊的事件,但在 RecyclerView
的情境下需要一點額外的功夫,因為我們真正在意的點擊通常是第幾個 RecyclerView
裡的第幾個 View
被點了而不是整個 RecyclerView
,所以 listener 必須一路傳到 ViewHolder
裡才行。
回到 MyAdapter
我們定義一個 interface 作為我們共同的介面。
interface OnItemClickListener {
fun onItemClick(index: Int)
}
MyViewHolder
也要加上 這個 OnItemClickListener
的變數並且在真正的 click 事件發生的時候,呼叫 onItemClick
,把點擊的位置回傳。
class MyViewHolder(itemView: View, private val listener: MyAdapter.OnItemClickListener?) :
RecyclerView.ViewHolder(itemView) {
private val textView: TextView = itemView.findViewById(R.id.myTextView)
fun setData(data: Int) {
textView.text = "This is #$data Row"
itemView.setOnClickListener {
listener?.onItemClick(data)
}
}
}
MyAdapter
作為中介者,一邊必須接收外部設定的 listener,同時也要把 listener 傳到 MyViewHolder
的建構子裡。
我們新增一個函式 setListener
跟修改 onCreateViewHolder
如下:
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
private var clickListener: OnItemClickListener? = null
fun setListener(listener: OnItemClickListener?) {
this.clickListener = listener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(
LayoutInflater.from(parent.context)
.inflate(
R.layout.item_myholder,
parent,
false
), clickListener
)
}
}
Adapter
裡的 listener 也會是由外部提供的,回到 MainActivity
修改如下:
val adapter = MyAdapter()
adapter.setListener(object : MyAdapter.OnItemClickListener {
override fun onItemClick(index: Int) {
startActivity(Intent(this@MainActivity, SecondActivity::class.java))
}
})
recyclerView.adapter = adapter
重新 build 一次應該就會發現,每次點擊就會打開一個新的 Activity
了。
如果很不幸的你發現有時候點擊 onItemClick
沒有被呼叫,恭喜你擁有神之手!!但如何解決呢?
我們回顧一下我們的 item_myholder.xml
會發現 TextView
的寬度是 wrap_content
,所以 RecyclerView
的右邊沒有文字的地方基本上都是點擊無效的區域,這個小地方有時候甚至有經驗的工程師也會漏掉,大家要特別小心呢!
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</TextView>
有時候我們會想要在每個子 View
中間加上一條分隔線,你也可以把這條線當成每個子 View
的一部分,然後在最後一個子 View
隱藏這條線。但這樣寫實在是對不起父母,Android 提供更好的方式來實作這條線,只要把 DividerItemDecoration
建立起來然後設定到 RecyclerView
即可。
val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)
挑戰:
addItemDecoration
接受的參數是任何繼承自RecyclerView.ItemDecoration
的 class ,大家可以試試看自己 custom 一個試試看喔。
當我們要支援多種不同的 ViewHolder
在同一個 Adapter
裡面,首先我們要讓 Adapter
知道每個位置的 View
是否不一樣。Android 提供 getItemViewType
這個函式給我們如下:
override fun getItemViewType(position: Int): Int {
return 0
}
如果二個位置有不同的 ViewHolder
,就應該回傳不同的 int。那在 onCreateViewHolder
裡就會拿到這些不同的 viewType 可以用來識別建立不同的 ViewHolder
。
我們來想想 MyAdapter
想要加上一個 header 跟 footer 的 View
應該要怎麼做。
首先先建立二個 xml item_header
跟 item_footer
來呈現我們的 header 跟 footer,然後修改 MyAdapter
如下:
class MyAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
interface OnItemClickListener {
fun onItemClick(index: Int)
}
private val data = (1..100).toList()
private var clickListener: OnItemClickListener? = null
override fun getItemViewType(position: Int): Int {
return when (position) {
0 -> TYPE_HEADER
data.size + 1 -> TYPE_FOOTER
else -> TYPE_ITEM
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
TYPE_HEADER -> return HeaderHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_header,
parent,
false
)
)
TYPE_FOOTER -> return FooterHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_footer,
parent,
false
)
)
else -> return MyViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_myholder,
parent,
false
), listener = clickListener
)
}
}
fun setListener(listener: OnItemClickListener?) {
this.clickListener = listener
}
override fun getItemCount(): Int {
return data.size + 2
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is MyViewHolder) {
holder.setData(data[position - 1])
}
}
companion object {
const val TYPE_ITEM = 0
const val TYPE_HEADER = 1
const val TYPE_FOOTER = 2
}
}
思維就是我們告訴系統全部的資料比原有的資料多二筆(header/footer),然後再拿第一筆資料的時候給 header、拿最後一筆資料的時候給 footer。
挑戰:大家可以想想我們要怎麼減少
+ 1
、- 1
、+ 2
這種比較難維護的程式碼。
另外一種想法是把 (header/footer)也當成資料塞在 data 裡,這樣所有的 index 就可以遵從 data 的順序囉。
當我們資料有更新的時候,無論是新增、刪除或修改,基本上 Adapter
不會自動反映變化,我們要主動的告訴 Adapter
我們要更新資料,因為資料的修改有很多種不同動作,大家可以參考以下的 functions。
public final void notifyDataSetChanged()
public final void notifyItemChanged(int position)
public final void notifyItemInserted(int position)
public final void notifyItemRemoved(int position)
public final void notifyItemRangeInserted(int positionStart, int itemCount)
public final void notifyItemRangeRemoved(int positionStart, int itemCount)
public final void notifyItemRangeChanged(int positionStart, int itemCount)
通知的範圍要盡可能的小,如只有修改一筆資料沒有必要使用
notifyDataSetChanged
這樣更新的效能就會比較好喔。
以上就是今天的內容囉,RecyclerView
是個非常好用也很常用的元件,希望大家可以多練習一下。
Android 十全大補已經正式出書上架囉!
有興趣的讀者歡迎參考:
https://www.tenlong.com.tw/products/9789864345786
感謝前輩無私的詳細解說!釐清了學習 Android 開發幾個月以來好多沒有通的地方,這系列真的是優質好文,造福眾生!