上个礼拜,我在公众号的某篇文章下面看到这样一条留言:
什么?holder.adapterPosition被划线不推荐使用了?难道是API被弃用了?我决定对这个问题好好研究一下,并加急写一篇文章进行分析。仔细一看,holder.adapterPosition这不就是我们平时在RecyclerView里面用于获取点击位置的方法么,常用写法如下:
holder.itemView.setOnClickListener {
val position = holder.adapterPosition
Log.d("TAG", "you clicked position $position")
}
这个方法相信每个人都用过不下千百遍,这种方法怎么会被废弃呢?于是我到Android的官网去查了一下文档,果然,getAdapterPosition()方法被标记成了废弃:
我帮大家翻译一下这段英文:这个方法当多个adapter嵌套时会存在歧义。如果你是在一个adapter的上下文中调用这个方法,你可能想要调用的是getBindingAdapterPosition()方法。如果你想获得的position是如同在RecyclerView中看到的那样,你应该调用getAbsoluteAdapterPosition()方法。看完这段解释是不是还是一脸懵逼?但我已经尽可能翻译得准确了。我在看完这段解释之后也是不能理解,为什么这个方法当多个adapter嵌套时会存在歧义?多个adapter嵌套让我容易联想到RecyclerView中嵌套RecyclerView,但是好像Google长久以来并不推荐这种做法,更不太可能为这种做法废弃API。百思不得其解的时候,我突然想起来前几天隔壁鸿洋大神的公众号里推荐了一篇文章,讲的是Google新推出了一个MergeAdapter。直觉告诉我,可能是和这个新功能有关。不过MergeAdapter是在RecyclerView 1.2.0版本中才新增的,而官网目前RecyclerView的最新稳定版本还是1.1.0。1.2.0还在alpha阶段,连beta阶段都没到:
库还没稳定,文档却先标为废弃了,Google这个做法也真是有点急不可耐。那么MergeAdapter到底有什么作用呢?我简单看了一下介绍就明白了,因为这就是我一直想要追求的功能啊!它的主要作用很简单,就是将多个Adapter合并到一起。你可能会说,为什么我的RecyclerView里面会有多个Adapter呢?那是因为你或许还没有遇到过这样的需求,而我就遇到了。两年前我在做giffun这个项目时,查看GIF图详情的界面就是使用RecyclerView来做的。
可能你没有想到这个界面会是一个RecyclerView,但是它确实就是如此,界面中的内容主要分成了如上图所示的3部分。那么一个RecyclerView中怎么能显示3种完全不同的内容呢?我当时是在Adapter当中使用了多种不同的viewType来实现的:
override fun getItemViewType(position: Int) = when (position) {
0 -> DETAIL_INFO
1 -> if (commentCount == -1) {
LOADING_COMMENTS
} else if (commentCount == 0 || commentCount == -2) {
NO_COMMENT
} else {
HOT_COMMENTS
}
2 -> ENTER_COMMENT
else -> super.getItemViewType(position)
}
可以看到,这里根据不同的position,返回了不同的viewType。当position是0的时候,返回DETAIL_INFO,也就是gif详情区域。当position是1的时候,返回LOADING_COMMENTS、NO_COMMENT、HOT_COMMENTS中的一种,用于展示评论内容。当position是2的时候,返回ENTER_COMMENT,也就是评论输入框区域。giffun的源码是完全公开的,你可以到这里查看这个类的完整代码:
https://github.com/guolindev/giffun/blob/master/main/src/main/java/com/quxianggif/feeds/adapter/FeedDetailMoreAdapter.kt
那么这种写法有没有什么问题呢?最主要的问题就是,代码耦合性太高了。其实这几种不同的viewType之间完全没有任何关联性,将它们都写到同一个Adapter当中会让这个类显得比较臃肿,后期也就更加难为维护。而MergeAdapter就是为了解决这种情况而出现的。它可以让你将几个业务逻辑没有关联的Adapter分开编写,最后再将它们合并到一起,并设置给RecyclerView。这里我准备使用一个非常简单的例子来演示一下MergeAdapter的用法。首先,确保你使用的RecyclerView版本不低于1.2.0-alpha02,否则是没有MergeAdapter这个类的:
dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02'
}
接下来创建两个非常简单的Adapter,一个TitleAdapter和一个BodyAdapter,待会我们会用MergeAdapter将这两个Adapter合并到一起。TitleAdapter代码如下:
class TitleAdapter(val items: List<String>) : RecyclerView.Adapter<TitleAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val text: TextView = view.findViewById(R.id.text)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
val holder = ViewHolder(view)
return holder
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.text.text = items[position]
}
}
这是一个Adapter最简单的实现,没有任何逻辑在里面,只是为了显示一行文字。item_view是个只包含一个TextView控件的简单布局,这里就不展示其中的代码了。然后BodyAdapter的代码如下:
class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val text: TextView = view.findViewById(R.id.text)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
val holder = ViewHolder(view)
return holder
}
override fun getItemCount() = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.text.text = items[position]
}
}
基本上就是复制过来的代码,和TitleAdapter没有什么区别。然后我们在MainActivity当中就可以这样使用了:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val titleItems = generateTitleItems()
val titleAdapter = TitleAdapter(titleItems)
val bodyItems = generateBodyItems()
val bodyAdapter = BodyAdapter(bodyItems)
val mergeAdapter = MergeAdapter(titleAdapter, bodyAdapter)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = mergeAdapter
}
private fun generateTitleItems(): List<String> {
val list = ArrayList<String>()
repeat(5) { index ->
list.add("Title $index")
}
return list
}
private fun generateBodyItems(): List<String> {
val list = ArrayList<String>()
repeat(20) { index ->
list.add("Body $index")
}
return list
}
}
可以看到,这里我编写了generateTitleItems()和generateBodyItems()这两个方法,分别用于给两个Adapter生成数据集。然后创建了TitleAdapter和BodyAdapter的实例,并使用MergeAdapter将它们合并到一起。合并的方式很简单,就是将你要合并的所有Adapter的实例都传入到MergeAdapter的构造方法当中即可。最后,将MergeAdapter设置到RecyclerView当中,整个过程结束。是不是非常简单?几乎和之前RecyclerView的用法没有任何区别。现在运行一下程序,效果如下图所示:
可以看到,TitleAdapter和BodyAdapter中的数据是合并到一起显示的,同时也就说明,我们的MergeAdapter已经成功生效了。到这里为止都还算很好理解,但是接下来,我要给大家一个灵魂拷问了。如果这时,我想要监听BodyAdapter中元素的点击事件,那么调用getAdapterPosition()方法,获得的到底是BodyAdapter中元素的点击位置,还是合并之后元素的点击位置呢?你会发现,这个时候getAdapterPosition()方法已经会造成歧义了,这也就是开篇那段英文所描述的问题。而解决办法当然也很简单,Google废弃了getAdapterPosition()方法,但是却又提供了getBindingAdapterPosition()和getAbsoluteAdapterPosition()这两个方法。从名字上就可以看出来了,一个是用于获取元素位于当前绑定Adapter的位置,一个是用于获取元素位于Adapter中的绝对位置。如果觉得我上面的解释还不够清楚,通过下面的示例看一下你立马就能明白了。我们修改BodyAdapter中的代码,在里面加入监听当前元素点击事件的代码,如下所示:
class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val position = holder.bindingAdapterPosition
Toast.makeText(parent.context, "You clicked body item $position", Toast.LENGTH_SHORT).show()
}
return holder
}
...
}
可以看到,这里调用的是getBindingAdapterPosition()方法,并通过Toast弹出当前点击元素的位置。运行一下程序,效果如下图所示:
很明显,我们获取到的点击位置是元素位于BodyAdapter中的位置。再修改一下BodyAdapter中的代码,将getBindingAdapterPosition()方法换成getAbsoluteAdapterPosition()方法:
class BodyAdapter(val items: List<String>) : RecyclerView.Adapter<BodyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_view, parent, false)
val holder = ViewHolder(view)
holder.itemView.setOnClickListener {
val position = holder.absoluteAdapterPosition
Toast.makeText(parent.context, "You clicked body item $position", Toast.LENGTH_SHORT).show()
}
return holder
}
...
}
然后重新运行程序,如下所示:
结果一目了解,获取到的点击位置是元素位于合并后Adapter中的位置。最后整理一下结论吧:
-
如果你没有使用MergeAdapter,那么getBindingAdapterPosition()和getAbsoluteAdapterPosition()方法的效果是一模一样的。
-
如果你使用了MergeAdapter,getBindingAdapterPosition()得到的是元素位于当前绑定Adapter的位置,而getAbsoluteAdapterPosition()方法得到的是元素位于合并后Adapter的绝对位置。