Androidエレガント処理繰り返しクリック

6725 ワード

一般的な携帯電話のAndroidアプリでは、主なインタラクティブな方法はクリックです.ユーザーがクリックすると、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)
        })
}

しかし、2つの指を使って2つの異なるボタンを同時にクリックするなど、ボタンの機能が新しくページを開くと、2つのページが新しく開く可能性があります.Rxjavaは、複数のコントロールではなく、単一のコントロールに対して重複クリック防止を実現するためです.
現在の処理方法
現在は時間判定を使用しており、時間範囲内で1回のクリックにのみ応答し、前回のクリック時間をActivity WindowのdecorViewに保存することで、1つの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) {
    //       
}

その他のシーン処理を繰り返しクリック
間接設定クリック
ビューに直接設定されたクリックリスニングに加えて、他の間接的にクリックを設定する場所にも、リッチテキストやリストなどの重複クリックを処理する必要があるシーンがあります.
このため,単一クリックをトリガするか否かを判断するコードを抽出し,単独で1つの方法とする.
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コントロール、アダプタにはサードパーティ製ライブラリBaseRecyclerView AdapterHelperがあります.
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) -> Unitlistener: 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
        )
    }
}

レイアウトファイルで単一クリックを設定します.


コードの中で1回のクリックを処理します:
class YourViewModel : ViewModel() {

    fun handleClick() {
        //       
    }
}

まとめ
ビューに直接クリックを設定する場所については、繰り返しクリックを処理してonSingleClickを使用し、繰り返しクリックを処理しなくても元のsetOnClickListenerを使用します.
間接的にクリックを設定する場所について、重複クリックを処理する必要がある場合は、determineTriggerSingleClickを使用して、単一クリックをトリガーするかどうかを判断します.
プロジェクトアドレス
single-click、使い心地がいいと思います.スターをけちけちしないでください.