How Android handles repeated clicks gracefully

  • 2021-12-12 05:27:42
  • OfStack

How to handle before the directory The current way of dealing with it Other scenarios deal with repeated clicks Indirect setting click
Rich text
List
Data binding
Summarize Project address

Android App on a mobile phone, the main interaction mode is click. After the user clicks, App may update UI in the page, open a new page or initiate a network request. Android system itself does not deal with repeated clicks. If users click many times in a short time, there may be problems such as opening multiple pages or repeatedly initiating network requests. Therefore, it is necessary to add code to deal with repeated clicks where it has an impact on repeated clicks.

Previous processing methods

Previously, the scheme of RxJava was used in the project, and the third-party library RxBinding was used 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)
  })
}

However, there is a problem with this. For example, if you click two different buttons with two fingers at the same time, the function of the buttons is to open a new page, so it is possible to open two new pages. Because Rxjava is designed to prevent repeated clicks for a single control, not multiple controls.

The current way of dealing with it

Now, we use time judgment, and only respond to one click in the time range. By saving the last click time to decorView in Activity Window, all View in one Activity share one last click time.


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
}

The default value of parameter isShareSingleClick is true, which means that this control shares 1 last click time with other controls in the same Activity, or it can be changed to false manually, which means that this control has 1 last click time alone.


mBinding.btn1.onSingleClick {
 //  Handle a single click 
}

mBinding.btn2.onSingleClick(interval = 2000, isShareSingleClick = false) {
 //  Handle a single click 
}

Other scenarios deal with repeated clicks

Indirect setting click

In addition to click listening set directly on View, other places where clicks are set indirectly also have scenarios that need to deal with repeated clicks, such as rich text and lists.

For this reason, the code for judging whether to trigger a single click is pulled out and used as a separate 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
) {
 ...
}

Call determineTriggerSingleClick directly in the click listening callback to determine whether a single click is triggered. Let's take rich text and list as examples.

Rich text

Inherit ClickableSpan and judge whether to trigger a single click in 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
)

There will be a problem in this way. widget in onClick callback is the control for setting rich text. That is to say, if there are multiple single clicks on rich text, even if isShareSingleClick value is false, these single clicks will still share the last click time of setting rich text control.

Therefore, special treatment is needed here. When isShareSingleClick is false, create a fake View to trigger the click event, so that multiple places where isShareSingleClick is false in rich text have their own fake View to enjoy the last click time.


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)
 }
 ...
}

Where rich text is set, use setting 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") }
 }
}

List

The list uses the RecyclerView control, and the adapter uses the third-party library BaseRecyclerViewAdapterHelper.

Item Click:


adapter.setOnItemClickListener { _, view, _ ->
 view.determineTriggerSingleClick {
  //  Handle a single click 
 }
}

Item Child Click:


adapter.addChildClickViewIds(R.id.btn1, R.id.btn2)
adapter.setOnItemChildClickListener { _, view, _ ->
 when (view.id) {
  R.id.btn1 -> {
   //  Handle ordinary clicks 
  }
  R.id.btn2 -> view.determineTriggerSingleClick {
   //  Handle a single click 
  }
 }
}

Data binding

When using DataBinding, sometimes click events will be set directly in the layout file, so add @ BindingAdapte annotation on View. onSingleClick to set single click events in the layout file and make adjustments to the code. At this time, it is necessary to set listener: (View)- > Replace 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
  )
 }
}

Set a single click in the layout file:


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
}
0

Handle a single click in code:


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
}
1

Summarize

For places where clicks are set directly on 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 places where clicks are set indirectly, determineTriggerSingleClick is used to determine whether a single click is triggered if repeated clicks need to be handled.

Project address

single-click, feel very cool to use, please don't be stingy with your Star!

These are the details of how Android handles repeated clicks gracefully. For more information about Android handling repeated clicks, please pay attention to other related articles on this site!


Related articles: