什麼?RecyclerView中獲取點擊位置的接口被廢棄了?

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個工作日都有文章更新。

各位小夥伴們,大家早上好。上個禮拜,我在公衆號的某篇文章下面看到這樣一條留言:

什麼?holder.adapterPosition被劃線不推薦使用了?

《第三行代碼》這纔剛剛出版,竟然就有API被棄用了,我決定對這個問題好好研究一下,並加急寫一篇文章進行分析。

仔細一看,holder.adapterPosition這不就是我們平時在RecyclerView裏面用於獲取點擊位置的方法麼,常用寫法如下:

holder.itemView.setOnClickListener {
	val position = holder.adapterPosition
	Log.d("TAG", "you clicked position $position")
}

這個方法相信每個人都用過不下千百遍,這種方法怎麼會被廢棄呢?於是我到Android的官網去查了一下文檔,果然,getAdapterPosition()方法被標記成了廢棄:

我幫大家翻譯一下這段英文:這個方法當多個adapter嵌套時會存在歧義。如果你是在一個adapter的上下文中調用這個方法,你可能想要調用的是getBindingAdapterPosition()方法。如果你想獲得的position是如同在RecyclerView中看到的那樣,你應該調用getAbsoluteAdapterPosition()方法。

看完這段解釋是不是還是一臉懵逼?但我已經儘可能翻譯得準確了。

我在看完這段解釋之後也是不能理解,爲什麼這個方法當多個adapter嵌套時會存在歧義?多個adapter嵌套讓我容易聯想到RecyclerView中嵌套RecyclerView,但是好像Google長久以來並不推薦這種做法,更不太可能爲這種做法廢棄API。

百思不得其解的時候,我突然想起來前幾天隔壁鴻洋大神的公衆號裏推薦了一篇文章,講的是Google新推出了一個MergeAdapter。直覺告訴我,可能是和這個新功能有關。

不過MergeAdapter是在RecyclerView 1.2.0版本中才新增的,而官網目前RecyclerView的最新穩定版本還是1.1.0。1.2.0還在alpha階段,連beta階段都沒到:

庫還沒穩定,文檔卻先標爲廢棄了,Google這個做法也真是有點急不可耐。

那麼MergeAdapter到底有什麼作用呢?我簡單看了一下介紹就明白了,因爲這就是我一直想要追求的功能啊!

它的主要作用很簡單,就是將多個Adapter合併到一起。

你可能會說,爲什麼我的RecyclerView裏面會有多個Adapter呢?那是因爲你或許還沒有遇到過這樣的需求,而我就遇到了。

兩年前我在做giffun這個項目時,查看GIF圖詳情的界面就是使用RecyclerView來做的。

可能你沒有想到這個界面會是一個RecyclerView,但是它確實就是如此,界面中的內容主要分成了如上圖所示的3部分。

那麼一個RecyclerView中怎麼能顯示3種完全不同的內容呢?我當時是在Adapter當中使用了多種不同的viewType來實現的:

override fun getItemViewType(position: Int) = when (position) {
	0 -> DETAIL_INFO
	1 -> if (commentCount == -1) {
		LOADING_COMMENTS
	} else if (commentCount == 0 || commentCount == -2) {
		NO_COMMENT
	} else {
		HOT_COMMENTS
	}
	2 -> ENTER_COMMENT
	else -> super.getItemViewType(position)
}

可以看到,這裏根據不同的position,返回了不同的viewType。當position是0的時候,返回DETAIL_INFO,也就是gif詳情區域。當position是1的時候,返回LOADING_COMMENTS、NO_COMMENT、HOT_COMMENTS中的一種,用於展示評論內容。當position是2的時候,返回ENTER_COMMENT,也就是評論輸入框區域。

giffun的源碼是完全公開的,你可以到這裏查看這個類的完整代碼:

https://github.com/guolindev/giffun/blob/master/main/src/main/java/com/quxianggif/feeds/adapter/FeedDetailMoreAdapter.kt

那麼這種寫法有沒有什麼問題呢?最主要的問題就是,代碼耦合性太高了。其實這幾種不同的viewType之間完全沒有任何關聯性,將它們都寫到同一個Adapter當中會讓這個類顯得比較臃腫,後期也就更加難爲維護。

而MergeAdapter就是爲了解決這種情況而出現的。它可以讓你將幾個業務邏輯沒有關聯的Adapter分開編寫,最後再將它們合併到一起,並設置給RecyclerView。

這裏我準備使用一個非常簡單的例子來演示一下MergeAdapter的用法。

首先,確保你使用的RecyclerView版本不低於1.2.0-alpha02,否則是沒有MergeAdapter這個類的:

dependencies {
    implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
}

接下來創建兩個非常簡單的Adapter,一個TitleAdapter和一個BodyAdapter,待會我們會用MergeAdapter將這兩個Adapter合併到一起。

TitleAdapter代碼如下:

class TitleAdapter(val items: List<String>) : RecyclerView.Adapter<TitleAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val text: TextView = view.findViewById(R.id.text)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        val holder = ViewHolder(view)
        return holder
    }

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.text.text = items[position]
    }

}

這是一個Adapter最簡單的實現,沒有任何邏輯在裏面,只是爲了顯示一行文字。item_view是個只包含一個TextView控件的簡單佈局,這裏就不展示其中的代碼了。

然後BodyAdapter的代碼如下:

class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {

    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val text: TextView = view.findViewById(R.id.text)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        val holder = ViewHolder(view)
        return holder
    }

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.text.text = items[position]
    }

}

基本上就是複製過來的代碼,和TitleAdapter沒有什麼區別。

然後我們在MainActivity當中就可以這樣使用了:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val titleItems = generateTitleItems()
        val titleAdapter = TitleAdapter(titleItems)

        val bodyItems = generateBodyItems()
        val bodyAdapter = BodyAdapter(bodyItems)

        val mergeAdapter = MergeAdapter(titleAdapter, bodyAdapter)

        recyclerView.layoutManager = LinearLayoutManager(this)
        recyclerView.adapter = mergeAdapter
    }

    private fun generateTitleItems(): List<String> {
        val list = ArrayList<String>()
        repeat(5) { index ->
            list.add("Title $index")
        }
        return list
    }

    private fun generateBodyItems(): List<String> {
        val list = ArrayList<String>()
        repeat(20) { index ->
            list.add("Body $index")
        }
        return list
    }
	
}

可以看到,這裏我編寫了generateTitleItems()和generateBodyItems()這兩個方法,分別用於給兩個Adapter生成數據集。然後創建了TitleAdapter和BodyAdapter的實例,並使用MergeAdapter將它們合併到一起。合併的方式很簡單,就是將你要合併的所有Adapter的實例都傳入到MergeAdapter的構造方法當中即可。

最後,將MergeAdapter設置到RecyclerView當中,整個過程結束。

是不是非常簡單?幾乎和之前RecyclerView的用法沒有任何區別。

現在運行一下程序,效果如下圖所示:

可以看到,TitleAdapter和BodyAdapter中的數據是合併到一起顯示的,同時也就說明,我們的MergeAdapter已經成功生效了。

到這裏爲止都還算很好理解,但是接下來,我要給大家一個靈魂拷問了。

如果這時,我想要監聽BodyAdapter中元素的點擊事件,那麼調用getAdapterPosition()方法,獲得的到底是BodyAdapter中元素的點擊位置,還是合併之後元素的點擊位置呢?

你會發現,這個時候getAdapterPosition()方法已經會造成歧義了,這也就是開篇那段英文所描述的問題。

而解決辦法當然也很簡單,Google廢棄了getAdapterPosition()方法,但是卻又提供了getBindingAdapterPosition()和getAbsoluteAdapterPosition()這兩個方法。從名字上就可以看出來了,一個是用於獲取元素位於當前綁定Adapter的位置,一個是用於獲取元素位於Adapter中的絕對位置。

如果覺得我上面的解釋還不夠清楚,通過下面的示例看一下你立馬就能明白了。

我們修改BodyAdapter中的代碼,在裏面加入監聽當前元素點擊事件的代碼,如下所示:

class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        val holder = ViewHolder(view)
        holder.itemView.setOnClickListener {
            val position = holder.bindingAdapterPosition
            Toast.makeText(parent.context, "You clicked body item $position", Toast.LENGTH_SHORT).show()
        }
        return holder
    }

    ...
}

可以看到,這裏調用的是getBindingAdapterPosition()方法,並通過Toast彈出當前點擊元素的位置。

運行一下程序,效果如下圖所示:

很明顯,我們獲取到的點擊位置是元素位於BodyAdapter中的位置。

再修改一下BodyAdapter中的代碼,將getBindingAdapterPosition()方法換成getAbsoluteAdapterPosition()方法:

class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
        val holder = ViewHolder(view)
        holder.itemView.setOnClickListener {
            val position = holder.absoluteAdapterPosition
            Toast.makeText(parent.context, "You clicked body item $position", Toast.LENGTH_SHORT).show()
        }
        return holder
    }

    ...
}

然後重新運行程序,如下所示:

結果一目瞭解,獲取到的點擊位置是元素位於合併後Adapter中的位置。

最後整理一下結論吧:

  1. 如果你沒有使用MergeAdapter,那麼getBindingAdapterPosition()和getAbsoluteAdapterPosition()方法的效果是一模一樣的。
  2. 如果你使用了MergeAdapter,getBindingAdapterPosition()得到的是元素位於當前綁定Adapter的位置,而getAbsoluteAdapterPosition()方法得到的是元素位於合併後Adapter的絕對位置。

文章寫到這裏,也就把開篇“木空”同學提出的問題徹底分析完畢,我覺得本篇文章也可以算得上是一篇《第一行代碼 第3版》的擴展文章吧。

另外說一下,由於《第一行代碼 第3版》已經出版,以後未來我自己編寫的所有文章都會使用Kotlin語言,Java就不再使用了,想學習Kotlin語言的朋友們可以考慮一下這本書。

由於這是我第一次嘗試編寫編程語言類型的內容,本來心裏不是特別有底,但是看到第一批讀者普遍反饋好評之後,我現在更加堅信這本書的質量了。我的QQ羣裏有個羣友還說,自己之前學過幾輪Kotlin了,都沒有這本書講得好,看得我也是心裏暖暖的。

文章末尾照例還是給出《第一行代碼 第3版》的購買鏈接,有需要的朋友點擊下方鏈接即可下單。

京東購買地址

噹噹購買地址

天貓購買地址


關注我的技術公衆號,每天都有優質技術文章推送。

微信掃一掃下方二維碼即可關注:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章