「RecyclerView中的位置」你真的會正確獲取Item的位置麼?

關於Position

咱們在使用使用 RecyclerView 的時候,老是不可避免的須要知道其 ItemView 的位置以實現各類各樣的需求:android

  • 設置點擊事件:咱們須要Item所處的位置,取得View對應的相關數據信息,進而完成點擊的交互操做。好比一個商品列表,點擊商品的Item時,咱們只有知道對應Item的位置,才能拿到Item的數據信息(譬如商品ID)從而跳轉至正確的商品詳情頁面。
  • 滾動列表至指定的Item位置:這種場景常被應用於RecyclerView的Item選中態發生變化時,滾動RecyclerView的位置,使得當前選中的Item能被用戶可見。此時咱們需知道Item相對於RecyclerView的位置,纔有可能滾動RecyclerView至正確的位置。

既然位置對於咱們平常開發這麼重要,那麼RecyclerView必定給咱們提供了獲取位置的API。沒錯,RecyclerView是提供了獲取位置的方法,還不止一種:web

  • onBindViewHolder(holder: ViewHolder, position: Int)
  • getAdapterPosition
  • getBindingAdapterPosition
  • getAbsoluteAdapterPosition
  • getLayoutPosition

你肯定你知道他們的具體含義、使用場景以及他們之間的區別麼?api

onBindViewHolder 中的 position 參數

一般咱們會在onBindViewHolder中經過postion參數綁定 data 和 View,像下面這樣:markdown

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
    holder.tvNumber.text = "Position: ${list[position]}"
}
複製代碼

很顯然,這麼作沒有任何問題(🐶保命)。less

可是若是在這裏使用position參數來處理點擊事件就會有點不合適了,咱們在上述的代碼中加一行代碼:ide

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
    holder.tvNumber.text = "Position: ${list[position]}"
    holder.itemView.setOnClickListener {
        Toast.makeText(it.context, "點擊了:${list[position]}", Toast.LENGTH_SHORT).show()
    }
}
複製代碼

而後在頁面中添加一個「-1」的按鈕,功能也很簡單:移除列表的第一項數據,代碼以下:佈局

fun removeFirstItem(){
    list.removeAt(0)
    notifyItemRemoved(0)
}
複製代碼

咱們來運行下看看效果:post

1627007428035.gif

能夠看到,若是代碼按照咱們預期那樣,應該是點擊哪一個位置,就彈出那個位置的position的toast,但是當咱們調用removeFirstItem方法移除列表的第一個item後,就會出現 item 和 position 對不上號的狀況(點擊了postion:1彈出的toast顯示點擊了:2),這就是在onBindViewHolder中直接使用position參數設置點擊事件可能引起的問題。性能

WHY?ui

其實緣由很簡單:使用notifyItem*()此類方法來刪除/添加/更改RecyclerView的數據中的任何一條數據時,RecyclerView並不會調用全部Item的onBindViewHolder方法更新item的位置,它只會更新notifyItem*()的位置,因此致使了顯示的數據和真實的數據 Position 對應不上的問題。

其實在官方源碼的註釋中也額外強調了這點(註釋很重要⚠️):

Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method again if the position of the item changes in the data set unless the item itself is invalidated or the new position cannot be determined. For this reason, you should only use the position parameter while acquiring the related data item inside this method and should not keep a copy of it. If you need the position of an item later on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will have the updated adapter position.

怎麼解決這個問題呢?其實源碼的註釋也給瞭解決方法了(註釋很重要⚠️),使用getAdapterPosition

getAdapterPosition

ViewHolder爲咱們提供了 getAdapterPosition 方法來獲取 ViewHolder 的位置。該方法 老是 返回 ViewHolder 最新的位置,也就意味着使用該方法,即便調用notifyItem*()此類方法來刪除/添加/更改 RecyclerView 的數據,該方法返回的位置也能確保獲取的Position是正確的。感興趣的能夠跟上面的寫法對比一下看看效果 ~

事情解決了...麼?

若是你看完我上一段的解決方法火燒眉毛的打開了Android Studio去驗證getAdapterPosition是否真的那麼有效,那我先要誇誇你,畢竟

紙上得來終覺淺,絕知此事要躬行。

因此你必定也知道了我要說什麼了:getAdapterPosition()被廢棄了,官網對此也有說明(官網很重要⚠️):

getAdapterPostion is deprecated.jpg

用我那蹩腳的英語大體翻譯一下,就是谷歌以爲這個方法在 Adapter 嵌套Adapter 的狀況下會帶來歧義,推薦你考慮使用 getBindingAdapterPosition或者getAbsoluteAdapterPosition這兩個方法。

相信你剛看完這段解釋的時候,必定是像我同樣更懵逼了:我原本只想知道爲啥棄用getAdapterPosition(),這傢伙倒好,又給我整出來倆方法,還歧義,等等...什麼是 Adapter 嵌套 Adapter?好傢伙,如今 Adapter 還能夠嵌套了麼?

你別說,還真能夠。若是你剛好使用過阿里開源的vLayout,就必定不會對 Adapter 嵌套 Adapter 的用法感到陌生。咱們都知道對於Android來講,複雜的Feed流頁面,咱們基本都是經過RecyclerView的多樣式佈局來實現,經過重寫Adapter的getItemViewType來區分不一樣的樣式,實現不一樣的UI邏輯,長久以來一直如此。

歷來如此,便對麼?

這種長久以來的寫法,最大的問題就是將不一樣樣式類型的佈局耦合在了同一個Adapter中,隨着業務的迭代,這個耦合的Adapter頗有可能變得異常臃腫,並且這種寫法要時刻注意數據的處理要區分ViewType,給往後的維護帶來極大的挑戰。有沒有更好的作法呢?

對於以上問題,阿里給出了vLayout庫來解決,這裏就不展開講了,由於——它中止維護了。谷歌大概是看到了開發者面對這種複雜頁面開發和維護時臉上的痛苦面具,因此他們推出了MergeAdapter這個玩意,簡單來講,他就像一個容器,裏面能夠添加多個Adapter,而後將MergeAdapter設置爲RecyclerView的Adapter,從而輕鬆實現多樣式佈局的效果。這就是谷歌官網所寫的 Adapter 嵌套 Adapter狀況:MergeAdapter 裏 可能會包含了 多個開發者寫的Adapter。

這種狀況下,咱們若是繼續調用getAdapterPosition就會引起歧義了,由於程序可能並不知道你想要的是ViewHolder的相對位置,仍是絕對位置

相對位置 & 絕對位置?getBindindAdapterPosition 與 getAbsoluteAdapterPosition 的區別

此處的相對位置及絕對位置的叫法,並不是官方叫法,而是參考文件系統中的 相對路徑 和 絕對路徑,提出的一種相似概念。咱們舉例說明什麼是相對位置和絕對位置。以下圖中的例子:MergeAdapter裏包含了A Adapter 和 B Adapter,在頁面的展現上,B 在 A 的後面,咱們想獲取B中某一個元素b3的位置,此時的位置有兩種:b3在B中的位置,我把他叫作相對位置,以及b3在整個RecyclerView中所處的位置,我將其稱之爲絕對位置。

getAbsoluteAdapterPosition & getBindingAdapterPostion.png 官方提供的兩個方法getBindingAdapterPostiongetAbsoluteAdapterPosition就是用來獲取ViewHolder的相對位置和絕對位置的。

  • getBindingAdapterPosition將會返回該ViewHolder相對於它綁定的Adapter中的位置,即相對位置。
  • getAbsoluteAdapterPosition將會返回該ViewHolder相對於RecyclerView的位置,即絕對位置。

回到咱們文章開頭提到的兩種典型的RecyclerView中使用Position的場景:

設置點擊事件 & 記錄、操做RecyclerView的滾動狀態,對於前者,咱們每每使用getBindingAdapterPostion獲取ViewHolder對應的數據項,完成點擊操做。

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
        holder.tvNumber.text = "Position: ${list[position]}"
        holder.itemView.setOnClickListener {
            Toast.makeText(it.context, "點擊了:${list[holder.bindingAdapterPosition]}", Toast.LENGTH_SHORT).show()
        }
    }
複製代碼

至於後者,很明顯,咱們應該使用getAbsoluteAdapterPosition來操縱RecyclerView的滾動。

固然,若是你的項目徹底沒有使用ConcatAdapter,那getBindingAdapterPostion和getAbsoluteAdapterPosition對於你來講,沒有任何區別,不過我仍推薦你按照不一樣的使用場景選用不一樣的方法獲取適合的位置參數,畢竟之後用不用ConcatAdapter 誰又說的清楚呢?

getLayoutPosition

那getLayoutPosition又是獲取什麼位置的呢?什麼場景下咱們使用該api來獲取位置呢?

getLayoutPosition,顧名思義,就是獲取該ViewHolder在實際佈局中的位置。咱們都知道,RecyclerView使用LayoutManager來管理數據集的現實。當開發者調用notifyData*()等方法通知RecyclerView刷新UI時,出於性能的考慮,RecyclerView的UI並不會馬上刷新,和Data保持一致,而是經過LayoutManager惰性更新相關佈局——這個過程伴隨着時間上的等待,一般狀況下,這個等待時間小於16ms。因此,從感官上講,getLayoutPosition與getAbsoluteAdapterPosition十分類似:getAbsoluteAdapterPosition返回的是該ViewHolder相對於RecyclerView的絕對位置,而getLayoutPosition返回的是該ViewHolder相對於RecyclerView實際佈局的絕對位置。

說具體點,就是adapter和layout的位置會有時間差(一般狀況下<16ms), 若是你改變了Adapter的數據而後刷新視圖, layout須要過一段時間纔會更新視圖, 在這段時間裏面, 這兩個方法返回的position會不同。

notifyDataSetChanged以後並不能立刻獲取Adapter中的position, 要等佈局結束以後才能獲取到.

而對於Layout的position, 在notifyItemInserted以後, Layout不能立刻獲取到新的position, 由於佈局還沒更新(須要<16ms的時間刷新視圖), 因此只能獲取到舊的, 可是Adapter中的position就能夠立刻獲取到最新的position。

因此,對於上面的點擊事件的場景,咱們在獲取用戶點擊位置的時候,使用getLayoutPosition可能效果更好,這樣,就能確保用戶點擊的始終是他看到的那個數據(消除16ms帶來的時間差問題),代碼能夠改形成下面這樣:

override fun onBindViewHolder(holder: NumberHolder, position: Int) {
        holder.tvNumber.text = "Position: ${list[position]}"
        holder.itemView.setOnClickListener {
            Toast.makeText(it.context, "點擊了:${list[holder.layoutPosition]}", Toast.LENGTH_SHORT).show()
        }
    }
複製代碼

總結

  • 源碼註釋很重要
  • 官網文檔很重要

遇到這種方法模棱兩可,讓人傻傻分不清楚的狀況,做爲API調用者的咱們,須要咱們作到的就是適當的閱讀源碼註釋,結合官方文檔,正確理解他們各自所表明的含義以及可能帶來的影響,合理使用他們。

相關文章
相關標籤/搜索