如何优雅的处理 Android 重复点击 [建议收藏]
Posted BUGgogogo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何优雅的处理 Android 重复点击 [建议收藏]相关的知识,希望对你有一定的参考价值。
现在的android APP 最主要的交互就是点击,但是在用户的使用过程中容易出现“重复点击”,导致页面多开,重复请求等问题,这样的情况该如何解决呢?
今天就和大家分享一下,Android 如何优雅处理重复点击的问题。
原文地址:https://www.jianshu.com/p/04ed8d18c335
一般手机上的 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 判断是否触发单次点击。
项目地址
single-click,觉得用起来很爽的,请不要吝啬你的 Star !
最后
B站视频系列:
- 【2021Android面试真题解析大合集】腾讯/阿里/百度/字节/京东……全收录
- FrameWork层源码分析
- Android项目实战
- Android高级UI实战
- 性能优化
- Android 底层原理解析
- Android零基础
技术文系列:
- 大厂Android面试官亲讲:Binder 相关问题到底该怎样回答
- Activity显示界面背后的故事:一文让你理清View的那些复杂关系
- 抖音Android岗面试性能优化篇之Rhea(新一代全能型性能分析工具)【速看】
以上是关于如何优雅的处理 Android 重复点击 [建议收藏]的主要内容,如果未能解决你的问题,请参考以下文章