共计 5517 个字符,预计需要花费 14 分钟才能阅读完成。
个别手机上的 Android App,次要的交互方式是点击。用户在点击后,App 可能做出在页面内更新 UI、新开一个页面或者发动网络申请等操作。Android 零碎自身没有对反复点击做解决,如果用户在短时间内屡次点击,则可能呈现新开多个页面或者反复发动网络申请等问题。因而,须要对反复点击有影响的中央,减少解决反复点击的代码。
之前的解决形式
之前在我的项目中应用的是 RxJava 的计划,利用第三方库 RxBinding 实现了避免反复点击:
fun View.onSingleClick(interval: Long = 1000L, listener: (View) -> Unit) {RxView.clicks(this) | |
.throttleFirst(interval, TimeUnit.MILLISECONDS) | |
.subscribe({listener.invoke(this) | |
}, {LogUtil.printStackTrace(it) | |
}) | |
} |
然而这样有一个问题,比方应用两个手指同时点击两个不同的按钮,按钮的性能都是新开页面,那么有可能会新开两个页面。因为 Rxjava 这种形式是针对单个控件实现避免反复点击,不是多个控件。
当初的解决形式
当初应用的是工夫判断,在工夫范畴内只响应一次点击,通过将上次单击工夫保留到 Activity Window 中的 decorView 里,实现一个 Activity 中所有的 View 共用一个上次单击工夫。
fun View.onSingleClick( | |
interval: Int = SingleClickUtil.singleClickInterval, | |
isShareSingleClick: Boolean = true, | |
listener: (View) -> Unit | |
) { | |
setOnClickListener {val target = if (isShareSingleClick) getActivity(this)?.window?.decorView ?: this else this | |
val millis = target.getTag(R.id.single_click_tag_last_single_click_millis) as? Long ?: 0 | |
if (SystemClock.uptimeMillis() - millis >= interval) { | |
target.setTag(R.id.single_click_tag_last_single_click_millis, SystemClock.uptimeMillis() | |
) | |
listener.invoke(this) | |
} | |
} | |
} | |
private fun getActivity(view: View): Activity? { | |
var context = view.context | |
while (context is ContextWrapper) {if (context is Activity) {return context} | |
context = context.baseContext | |
} | |
return null | |
} |
参数 isShareSingleClick 的默认值为 true,示意该控件和同一个 Activity 中其余控件共用一个上次单击工夫,也能够手动改成 false,示意该控件本人独享一个上次单击工夫。
mBinding.btn1.onSingleClick {// 解决单次点击} | |
mBinding.btn2.onSingleClick(interval = 2000, isShareSingleClick = false) {// 解决单次点击} |
其余场景解决反复点击
间接设置点击
除了间接在 View 上设置的点击监听外,其余间接设置点击的中央也存在须要解决反复点击的场景,比如说富文本和列表。
为此将判断是否触发单次点击的代码抽离进去,独自作为一个办法:
fun View.onSingleClick( | |
interval: Int = SingleClickUtil.singleClickInterval, | |
isShareSingleClick: Boolean = true, | |
listener: (View) -> Unit | |
) {setOnClickListener { determineTriggerSingleClick(interval, isShareSingleClick, listener) } | |
} | |
fun View.determineTriggerSingleClick( | |
interval: Int = SingleClickUtil.singleClickInterval, | |
isShareSingleClick: Boolean = true, | |
listener: (View) -> Unit | |
) {...} |
间接在点击监听回调中调用 determineTriggerSingleClick 判断是否触发单次点击。上面拿富文本和列表举例。
富文本
继承 ClickableSpan,在 onClick 回调中判断是否触发单次点击:
inline fun SpannableStringBuilder.onSingleClick(listener: (View) -> Unit, | |
isShareSingleClick: Boolean = true, | |
... | |
): SpannableStringBuilder = inSpans(object : ClickableSpan() {override fun onClick(widget: View) {widget.determineTriggerSingleClick(interval, isShareSingleClick, listener) | |
} | |
... | |
}, | |
builderAction = builderAction | |
) |
这样会有一个问题,onClick 回调中的 widget,就是设置富文本的控件,也就是说如果富文本存在多个单次点击的中央,就算 isShareSingleClick 值为 false,这些单次点击还是会共用设置富文本控件的上次单击工夫。
因而,这里须要非凡解决,在 isShareSingleClick 为 false 的时候,创立一个假的 View 来触发单击事件,这样富文本中多个单次点击 isShareSingleClick 为 false 的中央都有一个本人的假的 View 来独享上次单击工夫。
class SingleClickableSpan(...) : ClickableSpan() { | |
private var mFakeView: View? = null | |
override fun onClick(widget: View) {if (isShareSingleClick) {widget} else {if (mFakeView == null) {mFakeView = View(widget.context) | |
} | |
mFakeView!! | |
}.determineTriggerSingleClick(interval, isShareSingleClick, listener) | |
} | |
... | |
} |
在设置富文本的中央,应用设置 onSingleClick 实现单次点击:
mBinding.tvText.movementMethod = LinkMovementMethod.getInstance() | |
mBinding.tvText.highlightColor = Color.TRANSPARENT | |
mBinding.tvText.text = buildSpannedString {append("normalText") | |
onSingleClick({// 解决单次点击}) {color(Color.GREEN) {append("clickText") } | |
} | |
} |
列表
列表应用 RecyclerView 控件,适配器应用第三方库 BaseRecyclerViewAdapterHelper。
Item 点击:
adapter.setOnItemClickListener { _, view, _ -> | |
view.determineTriggerSingleClick {// 解决单次点击} | |
} |
Item Child 点击:
adapter.addChildClickViewIds(R.id.btn1, R.id.btn2) | |
adapter.setOnItemChildClickListener { _, view, _ -> | |
when (view.id) { | |
R.id.btn1 -> {// 解决一般点击} | |
R.id.btn2 -> view.determineTriggerSingleClick {// 解决单次点击} | |
} | |
} |
数据绑定
应用 DataBinding 的时候,有时会在布局文件中间接设置点击事件,于是在 View.onSingleClick 上减少 @BindingAdapte 注解,实现在布局文件中设置单次点击事件,并对代码做出调整,这个时候须要将我的项目中 listener: (View) -> Unit 替换成 listener: View.OnClickListener。
@BindingAdapter(*["singleClickInterval", "isShareSingleClick", "onSingleClick"], | |
requireAll = false | |
) | |
fun View.onSingleClick( | |
interval: Int? = SingleClickUtil.singleClickInterval, | |
isShareSingleClick: Boolean? = true, | |
listener: View.OnClickListener? = null | |
) {if (listener == null) {return} | |
setOnClickListener { | |
determineTriggerSingleClick(interval ?: SingleClickUtil.singleClickInterval, isShareSingleClick ?: true, listener) | |
} | |
} |
在布局文件中设置单次点击:
<androidx.appcompat.widget.AppCompatButton | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" | |
android:text="@string/btn" | |
app:isShareSingleClick="@{false}" | |
app:onSingleClick="@{()->viewModel.handleClick()}" | |
app:singleClickInterval="@{2000}" /> |
在代码中解决单次点击:
class YourViewModel : ViewModel() {fun handleClick() {// 解决单次点击} | |
} |
总结
对于间接在 View 上设置点击的中央,如果须要解决反复点击应用 onSingleClick,不须要解决反复点击则应用原来的 setOnClickListener。
对于间接设置点击的中央,如果须要解决反复点击,则应用 determineTriggerSingleClick 判断是否触发单次点击。
我的项目地址
https://github.com/TaylorKunZhang/single-click
,
原文链接:https://www.jianshu.com/p/04e…
文末
您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!