「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调用者的咱们,须要咱们作到的就是适当的阅读源码注释,结合官方文档,正确理解他们各自所表明的含义以及可能带来的影响,合理使用他们。

相关文章
相关标签/搜索