Android 获取、移除 View 的 OnClickList
之前在代码中设置的通过 View.isClickable 去控制 View 的重复点击,昨天突然发现即使控制了,仍然能够再次触发点击事件,让我很是懵逼。
后来翻阅一系列的资料之后,发现了 View.setOnClickListener 源码中的这段代码:
/**
* Register a callback to be invoked when this view is clicked. If this view is not
* clickable, it becomes clickable.
*
* @param l The callback that will run
*
* @see #setClickable(boolean)
*/
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
我不知道这里是在哪个版本改的,我只知道,以后不能通过 isClickable 属性去随性的控制点击事件了。 😤
因为我代码里很多都是通过 isClickable 属性控制,并且是统一控制,不可能去一个个的修改。所以,只能针对于这个问题去解决了。
通过 View 的源码,我们可以看到 它的 ClickListener 是传递给了 getListenerInfo() 返回值的 mOnClickListener 去处理。我们来看看这个方法返回的类型(在 View 源码的 5989 行)。
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
通过名字我们能猜出个大概,就是一些 监听事件的信息。来看一下对象内容(View.java 4208行):
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
protected OnScrollChangeListener mOnScrollChangeListener;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/
protected OnContextClickListener mOnContextClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
OnCapturedPointerListener mOnCapturedPointerListener;
}
根据刚刚 OnClickListener 事件处理的方式,再看到这个对象中的这么多参数,我们基本可以猜到 View 中的大多数 Listener 都交由到这里来存储控制了。再加上 ListenerInfo 是一个非公开的内部类,我们这里想要获取到 View 的 onClickListener( 或者其他) 事件,只能通过反射去操作了。
另外,因为 ListenerInfo 是 View 类中的对象,所以我们在获取该对象时,如果你的 操作类型 不是 View 本身,而是其子类(TextView、ImageView等等)的话,那么就需要获取父类中的属性对象或方法。
这里是获取当前对象
中的 某一个方法,会进行向上查找:
/**
* https://blog.csdn.net/zmx729618/article/details/51320688
*
* 获取当前类型的 某个方法
*/
fun getDeclaredMethod(obj: Any, methodName: String, vararg parameterTypes: Class<*>): Method? {
var method: Method? = null
var clazz: Class<*> = obj.javaClass
while (clazz != Any::class.java) {
try {
method = clazz.getDeclaredMethod(methodName, *parameterTypes)
// method?.isAccessible = true
return method
} catch (e: Exception) {
//这里甚么都不要做!并且这里的异常必须这样写,不能抛出去。
//如果这里的异常打印或者往外抛,则就不会执行clazz = clazz.getSuperclass(),最后就不会进入到父类中了
}
clazz = clazz.superclass
}
return null
}
通过上面方法,我们可以获取到 ListenerInfo 对象:
fun getListenerInfo(view: View): Any? {
val method = getDeclaredMethod(view, "getListenerInfo")
method?.isAccessible = true
val info = method?.invoke(view)
return info
}
接下来就是获取其对应的属性参数了,(我这里是调试的 OnClickListener ,可以根据自己需要获取相关属性)
fun getOnClickListener(view: View): View.OnClickListener? {
val info = getListenerInfo(view)
info?.let {
val m = getFieldValue(it, "mOnClickListener") as View.OnClickListener?
return m
}
return null
}
我这边的需求是暂时屏蔽掉 OnClickListener , 那么不能通过 isClickable 控制了,我就只能先把它设置为 null 了。
fun removeOnClickListener(view: View) {
val info = getListenerInfo(view)
info?.let {
setFieldValue(it, "mOnClickListener", null)
}
}
在实际应用场景中,操作流程大概是当 某个按钮点击之后,进行数据提交,在提交过程中,不允许该按钮再次触发事件。
下面是演示代码:
vc_tv_click.setOnClickListener {
Log.i("TAG", "ViewClick")
}
val click = getOnClickListener(vc_tv_click)
vc_btn_cancel.setOnClickListener {
removeOnClickListener(vc_tv_click)
}
vc_btn_reset.setOnClickListener {
vc_tv_click.setOnClickListener(click)
}
到此,基本该操作就完成了。
示例代码
在本篇博客中,我们既能获取到 OnClickListener (用于工具方法中,保存临时点击事件, 事件操作完之后,用来恢复点击事件) ,又能暂时移除 OnClickListener。
这种方式,我们也可以用来控制 Android 中重复点击的问题。
个人感觉这个应该比那些控制间隔时间 1s 、2s 内不触发点击事件要优雅许多。并且是从根源上处理 触发事件。