关于 View 的二次点击,估计是测试对程序员频率较高的 Bug 了,为此我们来探讨一下如何防止二次点击。
一、普遍解法
网上几乎所有的解法都是在 View 调用点击事件后,将其的 clickable
属性设置成 false
,然后设置一个定时器
什么的,在多少秒后再将其的 clickable
设置成 true
。
示例代码:
/**
* <让 View 在 @param time 内不可点击>
*/
fun View.sleep(time: Long = 200) {
this.isClickable = false
launch({
delay(time)
this.isClickable = true
})
}
上面是利用到了协程,还有诸多法门,本文只是探讨不作具体实现,所以就不一一演示。(Java 代码可以利用简单的 装饰者模式 进行封装)
那么上面的代码在实际的运用当中是这样的:
view.setOnclickListener{
doSmThing()
view.sleep()
}
这样的做法简单粗暴,也是最实际的,几乎能解决 99% 的问题。
二、RxView解法
把一个点击事件,以响应式的模型实现。RxBinding 实际上是作为 RxJava 的一种扩展,里面包含了多种关于 View 的 Observable 扩展,例如一个点击事件我们可以这么写:
导入的是RxBinding 4.0 的库,该库对kotlin完美的支持。
view.clicks().subscribe {
doSmThing()
}
实际上 view.clicks()
就是生成了一个持有该 View 的一个被观察者,里面帮助我们实现了点击事件,使得点击后会在下游——subscribe()
参数中的 订阅者 中实现。
那么既然他把点击事件包装成一个 Observable ,那么我们能做的事情一件都不会落下,例如加个 throttleFirst
操作符,那么就可以轻易的实现多少时间内只取第一次的事件:
btn_blueTooth.clicks().throttleFirst(1000,TimeUnit.SECONDS).subscribe {
doSmThing()
}
三、花里胡哨解法
我们可以通过动态代理,拿到点击事件的 Listener 对象,将其强制代理,然后做一些操作:
/**
* <View 防二次点击 (花里胡哨版本)>
*/
inline fun View.avoidClick(){
val listenerInfo = View::class.java.getDeclaredMethod("getListenerInfo").apply {
isAccessible = true }.invoke(this)
val clickListener = listenerInfo::class.java.getField("mOnClickListener").get(listenerInfo)
if (clickListener == null){
throw IllegalStateException("该方法必须在设置 onClickListener 之后")
}else{
val clickListenerProxy = View.OnClickListener {
clickListener::class.java.getMethod("onClick",View::class.java).invoke(clickListener,this@avoidClick)
this.sleep() //这里直接调用简单粗暴的方法
}
listenerInfo::class.java.getField("mOnClickListener").set(listenerInfo,clickListenerProxy)
}
}
具体使用:
view.setOnclickListener{
doSmThing()
}
view.avoidClick()
对比第一个解法,两个不同的实现,区别就是方法调用位置的不同。
其实对于 kotlin 来说,扩展方法确实是个好东西,所以我们还可以这样实现(再说一遍,在 Java 中可以利用装饰者模式做出同样的操作):
fun View.setAvoidClickListener(time:Long = 200,click:(View)->Unit){
this.setOnClickListener {
click(it)
this.sleep(time)
}
}
具体使用:
view.setAvoidClickListener(300) {
doSmTing()
}
这个做法似乎更加简单,规则没有那么多,而且性能比花里胡哨版本好多了……
但是有些时候会变得有点麻烦,比如说使用 DataBinding 的时候,Android SDK 给你提供的 android:onClick
就不能绑定了,到时候得需要自己实现自定义绑定。
四、探讨根源性的问题
说了这么久,其实我们可以确定两个规则:
- 如果是依据
clickable
来实现,在 View 的点击事件后,马上调用view.isClickable = false
,在某个条件之后,再调用view.isClickable = true
。 - 单独开一个线程,来进行对时间的计算,在满足条件之后,再回到原线程中执行点击事件。
某个条件 可以是很多种情况,例如说需要等到一个网络请求结束之后,你才可以再次点击;又比如发送验证码必须得等 60s 才能再次点击等等。
这些条件我们都是必须要贴切具体的需求,才能书写具体的代码,想到这里基本上可以确定:
防二次点击没有完美的解决方案。