iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
0

我們上一篇介紹了 RecyclerView 的基本使用,但是實務上可能會有更進階的需求,比如說我們可以要取得點擊某一個子 View 的事件,或是我們每個子 View 中間都要加上一條線,或是我們可能有些子 View 要長不一樣。又或者是我們需要動態增加或修改整個 RecyclerView 的資料。

OnClick

我們之前介紹過 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>

Divider

有時候我們會想要在每個子 View 中間加上一條分隔線,你也可以把這條線當成每個子 View 的一部分,然後在最後一個子 View 隱藏這條線。但這樣寫實在是對不起父母,Android 提供更好的方式來實作這條線,只要把 DividerItemDecoration 建立起來然後設定到 RecyclerView 即可。

val dividerItemDecoration = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
recyclerView.addItemDecoration(dividerItemDecoration)

挑戰:addItemDecoration 接受的參數是任何繼承自 RecyclerView.ItemDecoration 的 class ,大家可以試試看自己 custom 一個試試看喔。

Multiple ViewHolder

當我們要支援多種不同的 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_headeritem_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 的順序囉。

Notify......

當我們資料有更新的時候,無論是新增、刪除或修改,基本上 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 十全大補] RecyclerView
下一篇
[Android 十全大補] Fragment
系列文
Android 十全大補30

1 則留言

1
programming_rooky
iT邦見習生 0 級 ‧ 2019-10-18 07:10:05

感謝前輩無私的詳細解說!釐清了學習 Android 開發幾個月以來好多沒有通的地方,這系列真的是優質好文,造福眾生!

我要留言

立即登入留言