考虑了,直接绑定 View#setOnClickListener 实现函数;或传入View.OnClickListener 实例。
Kotlin 协程实现
调用
mBind.btnJaClickA.debounceClick(lifecycleScope) {
}
mBind.btnJaClickB.debounceClick(this) {
}
mBind.btnJaClickC.debounceClick(lifecycleScope, originBlock = {
})
mBind.btnJaClickD.debounceClick(this, originBlock = {
})
本实现,会后触发真实事件,连续点击时,看到的日志是这样的:
start
start
start
...
end
Handler + Runnable 实现
除了构建一个全局的 handler 对象; 或可以直接使用 View内的handler实例。
eg.this.handler.postDelayed()
调用
mBind.btnJaClickD.debounceClickWidthHandler {
}
mBind.btnJaClickE.debounceClickWidthHandler(originBlock = {
})
本实现,会后触发真实事件,连续点击时,看到的日志是这样的:
start
start remove
start remove
...
end
基于系统时间 实现
调用
mBind.btnJaClickG.clickWithTrigger {
}
mBind.btnJaClickH.clickWithTrigger(originBlock = {
})
本实现,会先触发真实事件,连续点击时,看到的日志是这样的:
start
end
start
start
...
setTag/getTag 可能引发异常
使用uuid-string,并获取 其hashCode,作为tag的key值;或者使用一个固定的int值作为key值;可能会报异常:IllegalArgumentException: The key must be an application-specific resource id;
看这个异常说明,就是需要定义特定的 资源id ,来用作 setTag/getTag的key。
ids.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="job_id"/>
<item type="id" name="runnable_id"/>
<item type="id" name="trigger_last_time_id"/>
</resources>
应用:
setTag(R.id.job_id, obj)
getTag(R.id.job_id)
最终完整代码
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.coroutineScope
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.util.loge
import com.stone.stoneviewskt.util.logi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* desc:
* author: stone
* email: [email protected]
* time: 2022/5/21 12:14
*/
/**
* 利用 CoroutineScope 防抖
* 若block中是 CoroutineScope.() 这样的,在每次调用 CoroutineScope 实例对象时,就会触发。
* 会在最后一次 松开超时后, 执行 end
*/
private var <T : View> T.mDebounceSuspendJob: Job?
get() = getTag(R.id.job_id) as? Job
set(value) {
setTag(R.id.job_id, value)
}
fun <T : View> T.debounceClick(coroutineScope: CoroutineScope, delayMs: Long = 600L, block: suspend (T) -> Unit) {
// 对于回调函数 block 中,要考虑 界面销毁,view 为null 的问题。 eg. 异步线程回调、网络请求回调
setOnClickListener {
mDebounceSuspendJob?.cancel()
mDebounceSuspendJob = coroutineScope.launch {
logi("start: ${
System.currentTimeMillis()}")
delay(delayMs)
logi("end: ${
System.currentTimeMillis()}")
block(this@debounceClick)
mDebounceSuspendJob = null
}
}
}
fun <T : View> T.debounceClick(owner: LifecycleOwner, delayMs: Long = 600L, block: suspend (T) -> Unit) {
debounceClick(owner.lifecycle.coroutineScope, delayMs, block)
}
fun <T : View> T.debounceClick(coroutineScope: CoroutineScope, originBlock: View.OnClickListener?, delayMs: Long = 600L) {
originBlock ?: return
debounceClick(coroutineScope, delayMs) {
originBlock.onClick(this) }
}
fun <T : View> T.debounceClick(owner: LifecycleOwner, originBlock: View.OnClickListener?, delayMs: Long = 600L) {
originBlock ?: return
debounceClick(owner.lifecycle.coroutineScope, originBlock, delayMs)
}
/**
* 利用 Handler + Runnable 防抖
*/
private var <T : View> T.mDebounceHandleRunnable: Runnable?
get() = getTag(R.id.runnable_id) as Runnable?
set(value) {
setTag(R.id.runnable_id, value)
}
fun <T : View> T.debounceClickWidthHandler(delayMs: Long = 600L, callback: (T) -> Unit) {
// 对于回调函数 callback 中,要考虑 界面销毁,view 为null 的问题。 eg. 异步线程回调、网络请求回调
setOnClickListener {
logi("start: ${
System.currentTimeMillis()} ${
this.handler}")
mDebounceHandleRunnable?.let {
loge("remove")
this.handler.removeCallbacks(it)
}
mDebounceHandleRunnable = Runnable {
logi("end: ${
System.currentTimeMillis()}")
callback(this)
mDebounceHandleRunnable = null
}
this.handler.postDelayed(mDebounceHandleRunnable!!, delayMs)
}
}
fun <T : View> T.debounceClickWidthHandler(delayMs: Long = 600L, originBlock: View.OnClickListener?) {
originBlock ?: return
debounceClickWidthHandler(delayMs) {
originBlock.onClick(this) }
}
/**
* 记录系统时间,以判断 是否能执行真实点击事件。
* 会一开始就触发真实回调,后面的连续快速点击,不会触发。
*/
private var <T : View> T.triggerLastTime: Long
get() = getTag(R.id.trigger_last_time_id)?.toString()?.toLong() ?: 0
set(value) {
setTag(R.id.trigger_last_time_id, value)
}
private fun <T : View> T.clickEnable(delayMs: Long): Boolean {
var flag = false
val currentClickTime = System.currentTimeMillis()
if (currentClickTime - triggerLastTime >= delayMs) {
flag = true
}
triggerLastTime = currentClickTime
return flag
}
@Suppress("UNCHECKED_CAST")
fun <T : View> T.clickWithTrigger(delayMs: Long = 600, block: (T) -> Unit) {
setOnClickListener {
logi("start: ${
System.currentTimeMillis()}")
if (clickEnable(delayMs)) {
logi("end: ${
System.currentTimeMillis()}")
block(it as T)
}
}
}
fun <T : View> T.clickWithTrigger(originBlock: View.OnClickListener?, time: Long = 600) {
originBlock ?: return
clickWithTrigger(time) {
originBlock.onClick(this) }
}