CymChad / CymChad/BaseRecyclerViewAdapterHelper
在Adapter显示完成后再设置[setOnItemClickListener]时对当前屏中item点击无效的bug
Nobody has claimed this yet.
- Dominant language
- Kotlin
- Stars
- 24.6k
- Forks
- 5.2k
- PR merge metrics
- No merged PRs in 30d
Description
BRVAH有个bug......就是设置itemClickListener时,如果apdater已经加载完成,这时再设置,对当前adapter这一屏item点击是无效的。因为绑定监听是在onCreateViewHolder中,adapter已经显示出来之后再设置,这个时候就不会再走onCreateViewHolder去绑定了,必须滚屏后重新触发onCreateViewHolder才行。解决方法的一个方案是在convert方法中手动调用bindViewClickListener,然后在setOnItemClickListener之后调用一次notifyDataSetChanged(),触发convert方法去执行bindViewClickListener进行绑定。
setOnItemLongClickListener、setOnItemChildClickListener、setOnItemChildLongClickListener都有这个问题。。
我在自定义的基类Adapter中封装了一下修复了这个问题
abstract class MyBaseQuickAdapter<T, VH : BaseViewHolder> @JvmOverloads constructor(
layoutResId: Int = 0,
data: MutableList<T>? = null,
) : BaseQuickAdapter<T, VH>(layoutResId, data) {
/**
* 当前ViewHolder(不包括Header类型的item)的position
*/
inline val BaseViewHolder.itemPosition
get() = adapterPosition - headerLayoutCount
var onItemClickListener2: OnItemClickListener2<T, VH>? = null
private set
var onItemBindListener: OnItemBindListener<T, VH>? = null
private set
private val brvahItemViewOnClickListeners = mutableMapOf<BaseViewHolder, View.OnClickListener>()
override fun bindViewClickListener(viewHolder: VH, viewType: Int) {
super.bindViewClickListener(viewHolder, viewType)
viewHolder.itemView.onClickListener?.apply {
brvahItemViewOnClickListeners[viewHolder] = this
}
}
final override fun convert(holder: VH, item: T) {
val position = holder.itemPosition
//清除重用item的属性
holder.itemView.setOnClickListener(null)
/**
* 修复brvah在Adapter显示完成后再设置[setOnItemClickListener]时对当前屏中item点击无效的bug
*/
bindViewClickListener(holder, -1)
onItemBindListener?.onItemBind(this, holder, holder.itemView, position, item)
convert(holder, item, position)
/**
* 保留子Adapter的itemView点击事件,并避免其点击事件覆盖父Adapter对itemView设置的点击事件
*/
holder.itemView.injectionOnClickListenerOnAfter {
Log.d("LmcTest", "MyBaseQuickAdapter onItemClick position=$position, item=$item")
onItemClickListener2?.onItemClick(this@MyBaseQuickAdapter,
holder,
holder.itemView,
position,
item)
onItemClick(holder, holder.itemView, position, item)
Log.d("LmcTest",
"position=$position, size=${brvahItemViewOnClickListeners.size}, brvahItemViewOnClickListeners=$brvahItemViewOnClickListeners")
brvahItemViewOnClickListeners[holder]?.onClick(this)
}
}
override fun onViewDetachedFromWindow(holder: VH) {
super.onViewDetachedFromWindow(holder)
brvahItemViewOnClickListeners.remove(holder)
Log.d("LmcTest", "brvahItemViewOnClickListeners size=${brvahItemViewOnClickListeners.size}")
}
override fun setOnItemClickListener(listener: OnItemClickListener?) {
super.setOnItemClickListener(listener)
listener?.apply { notifyDataSetChanged() }
}
override fun setOnItemLongClickListener(listener: OnItemLongClickListener?) {
super.setOnItemLongClickListener(listener)
listener?.apply { notifyDataSetChanged() }
}
override fun setOnItemChildClickListener(listener: OnItemChildClickListener?) {
super.setOnItemChildClickListener(listener)
listener?.apply { notifyDataSetChanged() }
}
override fun setOnItemChildLongClickListener(listener: OnItemChildLongClickListener?) {
super.setOnItemChildLongClickListener(listener)
listener?.apply { notifyDataSetChanged() }
}
/**
* @param position holder.adapterPosition - headerLayoutCount
*/
abstract fun convert(holder: VH, item: T, position: Int)
/**
* 子类继承Adapter时可以在Adapter内部重写此方法处理item点击事件
* @param holder VH
* @param item T
* @param position Int
*/
open fun onItemClick(holder: VH, view: View, position: Int, item: T) {}
fun setOnItemClickListener2(listener: OnItemClickListener2<T, VH>?) {
onItemClickListener2 = listener
}
fun setOnItemBindListener(listener: OnItemBindListener<T, VH>?) {
onItemBindListener = listener
/*
设置后,立即触发convert,进而触发[com.lmc.library.third.brvah.adapter.MyBaseQuickAdapter.OnItemBindListener]回调。
解决在adapter已经刷新完成的情况下设置监听,回调不触发的问题。
*/
listener?.apply { notifyDataSetChanged() }
}
/**
* 该监听器可以用于在Adapter外部对其设置item点击事件监听。
* (重写Adapter时,内部可以使用[onItemClick])
*/
fun interface OnItemClickListener2<T, VH : BaseViewHolder> {
fun onItemClick(
adapter: MyBaseQuickAdapter<T, VH>,
holder: VH,
view: View,
position: Int,
item: T,
)
}
/**
* 监听item绑定到Adapter事件
* @param T
* @param VH : BaseViewHolder
*/
fun interface OnItemBindListener<T, VH : BaseViewHolder> {
fun onItemBind(
adapter: MyBaseQuickAdapter<T, VH>,
holder: VH,
view: View,
position: Int,
item: T,
)
}
}
补充用到的工具方法:
/**
* 通过反射的方式获取View设置的OnClickListener点击事件监听器
*/
val View.onClickListener: View.OnClickListener?
get() = try {
ReflectUtils.reflect(this) //这个是一个开源库AndroidUtilCode中的工具类
.field("mListenerInfo")
.field("mOnClickListener")
.get()
} catch (e: NullPointerException) {//field方法内部可能会有空错误
null
}
/**
* 保留View之前设置的点击事件处理,并在其中附加处理逻辑。
* 示例:
* view.injectionOnClickListener {
* it?.onClick(this)//保留原onClick处理逻辑
* //自定义新的onClick处理逻辑
* }
*
* 注意,不要在某些场景中重复调用,这会导致多层嵌套并触发多次onClick事件。
* 如需要在Adapter的onBindViewHolder中调用,需要在调用此方法前清除itemView重用的点击监听器,再执行子Adapter的绑定方法设置itemView点击,再执行此方法。
* @receiver View
* @param action Function1<[@kotlin.ParameterName] OnClickListener?, Unit> 通过反射获取的旧OnClickListener,可以在代码块中对其进行调用。
*/
fun View.injectionOnClickListener(action: View.(oldOnClickListener: View.OnClickListener?) -> Unit): View.OnClickListener {
/*
注意onClickListener必须要用中间变量进行保存,如果直接在代码块中访问,触发点击事件时会造成无限递归
*/
val oldOnClickListener = onClickListener
val newOnClickListener = View.OnClickListener {
action(oldOnClickListener)
}
setOnClickListener(newOnClickListener)
return newOnClickListener
}
/**
* 保留View之前设置的点击事件处理,并在其中附加处理逻辑。
*
* 注意,不要在某些场景中重复调用,这会导致多层嵌套并触发多次onClick事件。
* 如需要在Adapter的onBindViewHolder中调用,需要在调用此方法前清除itemView重用的点击监听器,再执行子Adapter的绑定方法设置itemView点击,再执行此方法。
* @receiver View
* @param action [@kotlin.ExtensionFunctionType] Function1<View, Unit>
* @param afterOldOnClick Boolean 为true,在原点击事件之后处理;为false,在原点击事件之前处理。
*/
fun View.injectionOnClickListenerOnAfterOrBefore(
action: View.() -> Unit,
afterOldOnClick: Boolean = true,
) {
injectionOnClickListener {
if (!afterOldOnClick) action()
it?.onClick(this)
if (afterOldOnClick) action()
}
}
/**
* 保留View之前设置的点击事件处理,并在其中附加处理逻辑。
* (在View原监听器onClick事件之后执行)
* @receiver View
* @param action [@kotlin.ExtensionFunctionType] Function1<View, Unit>
*/
fun View.injectionOnClickListenerOnAfter(action: View.() -> Unit) {
injectionOnClickListenerOnAfterOrBefore(action)
}
然后这个Adapter可以拿去用......
补充了在Adapter外部监听convert的回调(OnItemBindListener)
并且可以在子类Adapter中对itemView设置点击事件,而不会覆盖父类的点击事件造成父类OnItemClickListener失效的问题
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at bindViewClickListener, onCreateViewHolder, convert, and the setOnItemClickListener, setOnItemLongClickListener, setOnItemChildClickListener, and setOnItemChildLongClickListener entry points described in the issue. Reproduce setting each listener after the adapter has displayed, then verify that current visible items respond without scrolling and that existing child item click handlers still work.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100