zoukankan      html  css  js  c++  java
  • 如何优雅的处理 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站视频系列:

    技术文系列:

  • 相关阅读:
    Linux搭建maven私服
    eclipse提交项目到GitHub
    eclipse安装sts插件
    idea配置jdk
    C语言之链表————(转载)
    链表(创建,插入,删除和打印输出(转载)
    C语言之链表
    ARM 之LCD和LCD控制器
    ARM的两种启动方式 (NAND FLASH. NOR FLASH)
    ARM 汇编器对C的扩展
  • 原文地址:https://www.cnblogs.com/chengsisi/p/15210213.html
Copyright © 2011-2022 走看看