For Android apps on mobile phones, the main way of interaction is click. After the user clicks, the App may update the UI within the page, open a new page or initiate network requests. The Android system itself does not deal with repeated clicks. If a user clicks multiple times in a short period of time, multiple pages may be opened or repeated network requests may be made. Therefore, we need to add code to handle repeated clicks where they are affected.
The way it was handled before
The previous RxJava scheme used in the project uses the third-party library RxBinding to prevent repeated clicks:
fun View.onSingleClick(interval: Long = 1000L, listener: (View) - >Unit) {
RxView.clicks(this)
.throttleFirst(interval, TimeUnit.MILLISECONDS)
.subscribe({
listener.invoke(this)
}, {
LogUtil.printStackTrace(it)
})
}
Copy the code
However, there is a problem with this, for example, using two fingers to click on two different buttons at the same time, the function of the button is to open a new page, so it is possible to open two new pages. Because the Rxjava approach is implemented for a single control to prevent repeated clicks, not multiple controls.
The way it’s handled now
We now use time judgment, which responds to a single click within the time range. By saving the time of the last click to the decorView in the Activity Window, all views in an Activity share the same time of the last click.
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
}
Copy the code
The default value of the isShareSingleClick parameter is true, which means that the control shares the last click time with other controls in the same Activity, or you can manually change it to False, which means that the control has its own last click time.
mBinding.btn1.onSingleClick {
// Handle a single click
}
mBinding.btn2.onSingleClick(interval = 2000, isShareSingleClick = false) {
// Handle a single click
}
Copy the code
Other scenarios handle repeated clicks
Indirect Settings click
In addition to the click-listening setup directly on the View, other indirect Settings for click-listening also have scenarios that need to handle repeated clicks, such as rich text and lists.
To do this, separate out the code that determines whether a single click is triggered as a single method:
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
){... }Copy the code
Directly in the click listener callback call determineTriggerSingleClick determine whether triggering a single click. Take rich text and lists for example.
The rich text
Using ClickableSpan to determine if a single click is triggered in the onClick callback:
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
)
Copy the code
One problem with this is that the widget in the onClick callback is the control that sets the rich text. That is, if the rich text has multiple single clicks, even if isShareSingleClick is false, those single clicks will share the time of the last click that set the rich text control.
So, the special treatment is to create a fake View to trigger the click event when isShareSingleClick is false, In this way, rich text with multiple single-click isShareSingleClick false will have its own fake View to keep the last click time to itself.
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)
}
...
}
Copy the code
Where rich text is set, use onSingleClick to achieve a single click:
mBinding.tvText.movementMethod = LinkMovementMethod.getInstance()
mBinding.tvText.highlightColor = Color.TRANSPARENT
mBinding.tvText.text = buildSpannedString {
append("normalText")
onSingleClick({
// Handle a single click
}) {
color(Color.GREEN) { append("clickText")}}}Copy the code
The list of
Use RecyclerView control list, library BaseRecyclerViewAdapterHelper adapter USES a third party.
Click the Item:
adapter.setOnItemClickListener { _, view, _ ->
view.determineTriggerSingleClick {
// Handle a single click}}Copy the code
Item Child click:
adapter.addChildClickViewIds(R.id.btn1, R.id.btn2)
adapter.setOnItemChildClickListener { _, view, _ ->
when (view.id) {
R.id.btn1 -> {
// Handle normal clicks
}
R.id.btn2 -> view.determineTriggerSingleClick {
// Handle a single click}}}Copy the code
Data binding
The @bindingAdapte annotation is added to View. OnSingleClick to set single-click events in the layout file and make adjustments to the code. Replace listener: (View) -> Unit with 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
)
}
}
Copy the code
Set single click in layout file:
<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}" />
Copy the code
Handling a single click in code:
class YourViewModel : ViewModel() {
fun handleClick(a) {
// Handle a single click}}Copy the code
conclusion
For places that set clicks directly on the View, use onSingleClick if you need to handle repeated clicks, and use the original setOnClickListener if you don’t need to handle repeated clicks.
For indirect clicks, if need to deal with repeated clicks, use the determineTriggerSingleClick determine whether triggering a single click.
The project address
Single-click, please don’t be stingy with your Star!
“Gold Digging 2021 Spring Recruitment Campaign”, click to view the activity details