關于Position
我們在使用使用 RecyclerView 的時候,總是不可避免的需要知道其 ItemView 的位置以實作各種各樣的需求:
設定點擊事件:我們需要Item所處的位置,取得View對應的相關資料資訊,進而完成點擊的互動操作,比如一個商品串列,點擊商品的Item時,我們只有知道對應Item的位置,才能拿到Item的資料資訊(譬如商品ID)從而跳轉至正確的商品詳情頁面,滾動串列至指定的Item位置:這種場景常被應用于RecyclerView的Item選中態發生變化時,滾動RecyclerView的位置,使得當前選中的Item能被用戶可見,此時我們需知道Item相對于RecyclerView的位置,才有可能滾動RecyclerView至正確的位置,
既然位置對于我們日常開發這么重要,那么RecyclerView一定給我們提供了獲取位置的API,沒錯,RecyclerView是提供了獲取位置的方法,還不止一種:
- onBindViewHolder(holder: ViewHolder, position: Int)
- getAdapterPosition
- getBindingAdapterPosition
- getAbsoluteAdapterPosition
- getLayoutPosition
你確定你知道他們的具體含義、使用場景以及他們之間的區別么?
onBindViewHolder 中的 position 引數
通常我們會在onBindViewHolder中通過postion引數系結 data 和 View,像下面這樣:
override fun onBindViewHolder(holder: NumberHolder, position: Int) {
holder.tvNumber.text = "Position: ${list[position]}"
}
很顯然,這么做沒有任何問題(🐶保命),
但是如果在這里使用position引數來處理點擊事件就會有點不合適了,我們在上述的代碼中加一行代碼:
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)
}
我們來運行下看看效果:

可以看到,如果代碼按照我們預期那樣,應該是點擊哪個位置,就彈出那個位置的position的toast,可是當我們呼叫removeFirstItem方法移除串列的第一個item后,就會出現 item 和 position 對不上號的情況(點擊了postion:1彈出的toast顯示點擊了:2),這就是在onBindViewHolder中直接使用position引數設定點擊事件可能引發的問題,
WHY?
其實原因很簡單:使用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
positionparameter 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()被廢棄了,官網對此也有說明(官網很重要??):

用我那蹩腳的英語大致翻譯一下,就是谷歌覺得這個方法在 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中所處的位置,我將其稱之為絕對位置,

官方提供的兩個方法getBindingAdapterPostion與getAbsoluteAdapterPosition就是用來獲取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呼叫者的我們,需要我們做到的就是適當的閱讀原始碼注釋,結合官方檔案,正確理解他們各自所代表的含義以及可能帶來的影響,合理使用他們,
小編自己在學習提升時,順帶從網上收集整理了一些 Android 開發相關的學習檔案、面試題、Android 核心筆記等等檔案,希望能幫助到大家學習提升,如有需要參考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 訪問查閱,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/294008.html
標籤:其他
上一篇:Android 人臉識別
