「RecyclerView中的位置」你真的会正确获取Item的位置么?

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了「RecyclerView中的位置」你真的会正确获取Item的位置么?相关的知识,希望对你有一定的参考价值。

好文推荐:
作者:沈剑心

关于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 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()被废弃了,官网对此也有说明(官网很重要⚠️):

用我那蹩脚的英语大致翻译一下,就是谷歌觉得这个方法在 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中所处的位置,我将其称之为绝对位置。

官方提供的两个方法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调用者的我们,需要我们做到的就是适当的阅读源码注释,结合官方文档,正确理解他们各自所代表的含义以及可能带来的影响,合理使用他们。

小编自己在学习提升时,顺带从网上收集整理了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 GitHub地址:https://github.com/733gh/Android-T3 访问查阅。

以上是关于「RecyclerView中的位置」你真的会正确获取Item的位置么?的主要内容,如果未能解决你的问题,请参考以下文章

关于recyclerView中ViewHolder中的几种不同方式获取position有什么区别

关于recyclerView中ViewHolder中的几种不同方式获取position有什么区别

关于recyclerView中ViewHolder中的几种不同方式获取position有什么区别

过滤recyclerview在刷卡时没有得到正确的卡片位置

滑动删除Item,拖拽切换Item,你想了解的都在这儿

RecyclerView的行项目视图隐藏/显示在Recyclerview的滚动上搞砸了