前言
RecyclerView
的 ViewHolder
中有好几个关于 Item 位置相关的属性(以下为 Kotlin 示例):
ViewHolder.adapterPosition
ViewHolder.layoutPosition
override fun onBindViewHolder(holder: ItemViewHolder, position: Int)
当我们需要使用“位置”信息时,到底应该使用哪个属性呢?考虑到有些人比较忙,可能没时候看完全文,因此在这里先说下结论:
绑定数据时,使用
ViewHolder.adapterPosition
,点击 Item 获取位置时,使用ViewHolder.layoutPosition
。
好了,送走了大忙人,接下来我们来好好说下它们的用法和区别:
onBindViewHolder
回调中的 position
参数
我们可以用该参数来绑定数据,像下面这样:
但是如果你想在点击事件中使用“位置”信息的话,就不能用 position
了。如果你确实在点击事件的回调中使用了 position
的话,可能很多时候你并没有感觉到有什么不对,实际体验感觉一切都挺正常的话,也没有出错。不过这样做确实在某些情况下会引起错误,可能是你还没有遇到而已,要不然 Google 也不会将该属性标记成 Deprecated
。
以 Kotlin
为例,在点击事件中使用 position
的话 Android Stduio
会提示如下警告:
‘getter for position: Int’ is deprecated. Deprecated in Java
This method is deprecated because its meaning is ambiguous due to the async handling of adapter updates. You should use getLayoutPosition() or getAdapterPosition() depending on your use case.
简单翻译下,该属性被废弃的原因:由于更新 adapter
的操作是异步的,因此该属性值会有不确定性。根据实际情况应该使用 getLayoutPosition()
或 getAdapterPosition()
来代替。
通常情况下,你需要使用的是 ViewHolder.adapterPosition
。不过,如果你在 Item 上应用了动画效果的话(例如,删除 Item 动画等),那么你应该使用的是 ViewHolder.layoutPosition
。
为什么点击事件中不能直接使用 onBindViewHolder
回调中的 position
参数呢?试想一下,如果你添加/删除或修改了 RecyclerView
中的数据,并且调用了 notifyItem**
方法(例如:notifyItemRangeInserted
,notifyItemRemoved
等)来通知 RecyclerView
中的数据已经发生了变化,这样的话 onBindViewHolder
回调中仅仅会得到新增加的 Item 的位置。让我们先来看看如下示例:
下图是首次加载 Item 时,onBindViewHolder
回调参数中的 position
值情况:
下图是向 RecyclerView
的顶部添加几条数据并调用 notifyItemRangeInserted
更新数据后,onBindViewHolder
回调参数中的 position
值情况:
可以看到每次添加数据后,只有对于新增的数据 onBindViewHolder
回调才会被调用。试想一下,如果 RecyclerView
已经加载了 10 条数据,其 position
分别是 0
到 9
。如果你使用了该 position
绑定了点击事件,之后你又向 RecyclerView
顶部添加了一条数据,其位置为 0
,并且调用了 notifyItemInserted
通知 RecyclerView
数据已经变化了,由于 adapter
更新数据的操作是异步的,那么之前旧数据的 position
可能还没有被更新,这会导致实际每个 Item 的 position
变成了 0 0 1 2 3 4 5 6 7 8 9
,而不是期望的 0 1 2 3 4 5 6 7 8 9 10
。因此在点击事件中不能直接使用 onBindViewHolder
回调中的 position
参数,因为这样可能会引起错误。
ViewHolder.adapterPosition
ViewHolder
提供了 adapterPosition
属性,该值可以保证总能得到 adapter
更新后的位置。这就意味着无论你什么时候点击 Item,你总可以得到最新的位置信息,就像下面这样:
不过需要注意的是,如果你在 Item 上应用了动画效果的话(例如,新增/删除 Item 动画等),那么你应该使用的是 ViewHolder.layoutPosition
,不能使用 ViewHolder.adapterPosition
方法,原因见后文。
ViewHolder.layoutPosition
RecyclerView
将数据集与数据集的显示隔离开来,这就是为什么我们可以通过 LayoutManager
来控制如何显示数据。RecyclerView
更新 Layout 是异步的,因此它需要先等待数据准备好之后才能更新变化后的 Layout。该操作通常会在 16ms 内完成,因此大多数时候 adapterPosition
和 layoutPosition
的值都是一样的。但是有时候我们需要知道更新 Layout 之后的 Item 的最新位置(例如,新增或删除 Item 时有动画效果,在动画过程中,用户又点击了其它 Item),此时就需要使用该属性了。
请看下面使用 layoutPosition
的示例,注意“Demo String 2”这条数据,它的原始位置是 1
(第一条数据的位置为 0
)。在新增或删除其它数据的同时,我们快速点击该数据,会发现始终能取得该数据的最新位置。
再来看下使用 adapterPosition
时的示例,注意“Demo String 3”这条数据,它的原始位置是 2
(第一条数据的位置为 0
)。在我追加数据的同时,点击该数据,会发现它的位置还是正确的,直到它的位置变成 5
时,此时刚好在我删除数据的同时,再次点击该数据,会发现它的位置依然还是上次的位置 5
,并没有取到最新的位置 4
。
看了上面两个示例的对比之后,你就应该明白为什么在 Item 上应用动画效果后,应该使用 ViewHolder.layoutPosition
而不能使用 ViewHolder.adapterPosition
。
总结
一句话总结下我在使用各个“位置”属性时的原则:
绑定数据时,使用 ViewHolder.adapterPosition
,点击 Item 获取位置时,使用 ViewHolder.layoutPosition
。
源代码
需要源码的朋友,可以从我的 Gitee 上下载。