RecyclerView ViewHolder 关于 Item 位置相关的不同属性的区别

前言

  RecyclerViewViewHolder 中有好几个关于 Item 位置相关的属性(以下为 Kotlin 示例):

  1. ViewHolder.adapterPosition

  2. ViewHolder.layoutPosition

  3. override fun onBindViewHolder(holder: ItemViewHolder, position: Int)

  当我们需要使用“位置”信息时,到底应该使用哪个属性呢?考虑到有些人比较忙,可能没时候看完全文,因此在这里先说下结论:

绑定数据时,使用 ViewHolder.adapterPosition,点击 Item 获取位置时,使用 ViewHolder.layoutPosition

  好了,送走了大忙人,接下来我们来好好说下它们的用法和区别:

onBindViewHolder 回调中的 position 参数

  我们可以用该参数来绑定数据,像下面这样:

onBindViewHolder-position

  但是如果你想在点击事件中使用“位置”信息的话,就不能用 position 了。如果你确实在点击事件的回调中使用了 position 的话,可能很多时候你并没有感觉到有什么不对,实际体验感觉一切都挺正常的话,也没有出错。不过这样做确实在某些情况下会引起错误,可能是你还没有遇到而已,要不然 Google 也不会将该属性标记成 Deprecated

  以 Kotlin 为例,在点击事件中使用 position 的话 Android Stduio会提示如下警告:

click-onBindViewHolder-position

‘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** 方法(例如:notifyItemRangeInsertednotifyItemRemoved 等)来通知 RecyclerView 中的数据已经发生了变化,这样的话 onBindViewHolder 回调中仅仅会得到新增加的 Item 的位置。让我们先来看看如下示例:

  下图是首次加载 Item 时,onBindViewHolder 回调参数中的 position 值情况:

onBindViewHolder-position

  下图是向 RecyclerView 的顶部添加几条数据并调用 notifyItemRangeInserted 更新数据后,onBindViewHolder 回调参数中的 position 值情况:

InsertItemOnTop-position

  可以看到每次添加数据后,只有对于新增的数据 onBindViewHolder 回调才会被调用。试想一下,如果 RecyclerView 已经加载了 10 条数据,其 position 分别是 09。如果你使用了该 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,你总可以得到最新的位置信息,就像下面这样:

click-onBindViewHolder-position.png

  不过需要注意的是,如果你在 Item 上应用了动画效果的话(例如,新增/删除 Item 动画等),那么你应该使用的是 ViewHolder.layoutPosition,不能使用 ViewHolder.adapterPosition 方法,原因见后方。

ViewHolder.layoutPosition

  RecyclerView 将数据集与数据集的显示隔离开来,这就是为什么我们可以通过 LayoutManager 来控制如何显示数据。RecyclerView 更新 Layout 是异步的,因此它需要先等待数据准备好之后才能更新变化后的 Layout。该操作通常会在 16ms 内完成,因此大多数时候 adapterPositionlayoutPosition 的值都是一样的。但是有时候我们需要知道更新 Layout 之后的 Item 的最新位置(例如,新增或删除 Item 时有动画效果,在动画过程中,用户又点击了其它 Item),此时就需要使用该属性了。

  请看下面使用 layoutPosition 的示例,注意“Demo String 2”这条数据,它的原始位置是 1(第一条数据的位置为 0)。在新增或删除其它数据的同时,我们快速点击该数据,会发现始终能取得该数据的最新位置。

layoutPosition-demo

  再来看下使用 adapterPosition 时的示例,注意“Demo String 3”这条数据,它的原始位置是 2(第一条数据的位置为 0)。在我追加数据的同时,点击该数据,会发现它的位置还是正确的,直到它的位置变成 5 时,此时刚好在我删除数据的同时,再次点击该数据,会发现它的位置依然还是上次的位置 5,并没有取到最新的位置 4

adapterPosition-demo

  看了上面两个示例的对比之后,你就应该明白为什么在 Item 上应用动画效果后,应该使用 ViewHolder.layoutPosition 而不能使用 ViewHolder.adapterPosition

总结

  一句话总结下我在使用各个“位置”属性时的原则:

绑定数据时,使用 ViewHolder.adapterPosition,点击 Item 获取位置时,使用 ViewHolder.layoutPosition

源代码

  需要源码的朋友,可以从我的 Gitee 上下载。

参考文献

坚持原创及高品质技术分享,您的支持将鼓励我继续创作!