I have a problem with my app that if the user clicks the button multiple times quickly, then multiple events are generated before even my dialog holding the button disappear
Here's a 'debounced' onClick listener that I wrote recently.
You tell it what the minimum acceptable number of milliseconds between clicks is.
Implement your logic in onDebouncedClick instead of onClick
import android.os.SystemClock;
import android.view.View;
import java.util.Map;
import java.util.WeakHashMap;
/**
* A Debounced OnClickListener
* Rejects clicks that are too close together in time.
* This class is safe to use as an OnClickListener for multiple views, and will debounce each one separately.
*/
public abstract class DebouncedOnClickListener implements View.OnClickListener {
private final long minimumIntervalMillis;
private Map<View, Long> lastClickMap;
/**
* Implement this in your subclass instead of onClick
* @param v The view that was clicked
*/
public abstract void onDebouncedClick(View v);
/**
* The one and only constructor
* @param minimumIntervalMillis The minimum allowed time between clicks - any click sooner than this after a previous click will be rejected
*/
public DebouncedOnClickListener(long minimumIntervalMillis) {
this.minimumIntervalMillis = minimumIntervalMillis;
this.lastClickMap = new WeakHashMap<>();
}
@Override
public void onClick(View clickedView) {
Long previousClickTimestamp = lastClickMap.get(clickedView);
long currentTimestamp = SystemClock.uptimeMillis();
lastClickMap.put(clickedView, currentTimestamp);
if(previousClickTimestamp == null || Math.abs(currentTimestamp - previousClickTimestamp) > minimumIntervalMillis) {
onDebouncedClick(clickedView);
}
}
}
A Handler based throttler from Signal App.
import android.os.Handler;
import android.support.annotation.NonNull;
/**
* A class that will throttle the number of runnables executed to be at most once every specified
* interval.
*
* Useful for performing actions in response to rapid user input where you want to take action on
* the initial input but prevent follow-up spam.
*
* This is different from a Debouncer in that it will run the first runnable immediately
* instead of waiting for input to die down.
*
* See http://rxmarbles.com/#throttle
*/
public final class Throttler {
private static final int WHAT = 8675309;
private final Handler handler;
private final long thresholdMs;
/**
* @param thresholdMs Only one runnable will be executed via {@link #publish} every
* {@code thresholdMs} milliseconds.
*/
public Throttler(long thresholdMs) {
this.handler = new Handler();
this.thresholdMs = thresholdMs;
}
public void publish(@NonNull Runnable runnable) {
if (handler.hasMessages(WHAT)) {
return;
}
runnable.run();
handler.sendMessageDelayed(handler.obtainMessage(WHAT), thresholdMs);
}
public void clear() {
handler.removeCallbacksAndMessages(null);
}
}
Example usage:
throttler.publish(() -> Log.d("TAG", "Example"));
Example usage in an OnClickListener:
view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));
Example Kt usage:
view.setOnClickListener {
throttler.publish {
Log.d("TAG", "Example")
}
}
Or with an extension:
fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) {
throttler.publish(function)
}
Then example usage:
view.setThrottledOnClickListener(throttler) {
Log.d("TAG", "Example")
}
I use this class together with databinding. Works great.
/**
* This class will prevent multiple clicks being dispatched.
*/
class OneClickListener(private val onClickListener: View.OnClickListener) : View.OnClickListener {
private var lastTime: Long = 0
override fun onClick(v: View?) {
val current = System.currentTimeMillis()
if ((current - lastTime) > 500) {
onClickListener.onClick(v)
lastTime = current
}
}
companion object {
@JvmStatic @BindingAdapter("oneClick")
fun setOnClickListener(theze: View, f: View.OnClickListener?) {
when (f) {
null -> theze.setOnClickListener(null)
else -> theze.setOnClickListener(OneClickListener(f))
}
}
}
}
And my layout looks like this
<TextView
app:layout_constraintTop_toBottomOf="@id/bla"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center"
android:textSize="18sp"
app:oneClick="@{viewModel::myHandler}" />
With RxBinding it can be done easily. Here is an example:
RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> {
// action on click
});
Add the following line in build.gradle to add RxBinding dependency:
compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Here's a simple example:
public abstract class SingleClickListener implements View.OnClickListener {
private static final long THRESHOLD_MILLIS = 1000L;
private long lastClickMillis;
@Override public void onClick(View v) {
long now = SystemClock.elapsedRealtime();
if (now - lastClickMillis > THRESHOLD_MILLIS) {
onClicked(v);
}
lastClickMillis = now;
}
public abstract void onClicked(View v);
}
My solution, need to call removeall when we exit (destroy) from the fragment and activity:
import android.os.Handler
import android.os.Looper
import java.util.concurrent.TimeUnit
//single click handler
object ClickHandler {
//used to post messages and runnable objects
private val mHandler = Handler(Looper.getMainLooper())
//default delay is 250 millis
@Synchronized
fun handle(runnable: Runnable, delay: Long = TimeUnit.MILLISECONDS.toMillis(250)) {
removeAll()//remove all before placing event so that only one event will execute at a time
mHandler.postDelayed(runnable, delay)
}
@Synchronized
fun removeAll() {
mHandler.removeCallbacksAndMessages(null)
}
}