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 something that will work with any event, not just clicks. It will also deliver the last event even if it's part of a series of rapid events (like rx debounce).
class Debouncer(timeout: Long, unit: TimeUnit, fn: () -> Unit) {
private val timeoutMillis = unit.toMillis(timeout)
private var lastSpamMillis = 0L
private val handler = Handler()
private val runnable = Runnable {
fn()
}
fun spam() {
if (SystemClock.uptimeMillis() - lastSpamMillis < timeoutMillis) {
handler.removeCallbacks(runnable)
}
handler.postDelayed(runnable, timeoutMillis)
lastSpamMillis = SystemClock.uptimeMillis()
}
}
// example
view.addOnClickListener.setOnClickListener(object: View.OnClickListener {
val debouncer = Debouncer(1, TimeUnit.SECONDS, {
showSomething()
})
override fun onClick(v: View?) {
debouncer.spam()
}
})
1) Construct Debouncer in a field of the listener but outside of the callback function, configured with timeout and the callback fn that you want to throttle.
2) Call your Debouncer's spam method in the listener's callback function.
You can use this project: https://github.com/fengdai/clickguard to resolve this problem with a single statement:
ClickGuard.guard(button);
UPDATE: This library is not recommended any more. I prefer Nikita's solution. Use RxBinding instead.
So, this answer is provided by ButterKnife library.
package butterknife.internal;
import android.view.View;
/**
* A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
* same frame. A click on one button disables all buttons for that frame.
*/
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = () -> enabled = true;
@Override public final void onClick(View v) {
if (enabled) {
enabled = false;
v.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
This method handles clicks only after previous click has been handled and note that it avoids multiple clicks in a frame.
I would say the easiest way is to use a "loading" library like KProgressHUD.
https://github.com/Kaopiz/KProgressHUD
The first thing at the onClick method would be to call the loading animation which instantly blocks all UI until the dev decides to free it.
So you would have this for the onClick action (this uses Butterknife but it obviously works with any kind of approach):
Also, don't forget to disable the button after the click.
@OnClick(R.id.button)
void didClickOnButton() {
startHUDSpinner();
button.setEnabled(false);
doAction();
}
Then:
public void startHUDSpinner() {
stopHUDSpinner();
currentHUDSpinner = KProgressHUD.create(this)
.setStyle(KProgressHUD.Style.SPIN_INDETERMINATE)
.setLabel(getString(R.string.loading_message_text))
.setCancellable(false)
.setAnimationSpeed(3)
.setDimAmount(0.5f)
.show();
}
public void stopHUDSpinner() {
if (currentHUDSpinner != null && currentHUDSpinner.isShowing()) {
currentHUDSpinner.dismiss();
}
currentHUDSpinner = null;
}
And you can use the stopHUDSpinner method in the doAction() method if you so desire:
private void doAction(){
// some action
stopHUDSpinner()
}
Re-enable the button according to your app logic: button.setEnabled(true);
Here is a pretty simple solution, which can be used with lambdas:
view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));
Here is the copy/paste ready snippet:
public class DebounceClickListener implements View.OnClickListener {
private static final long DEBOUNCE_INTERVAL_DEFAULT = 500;
private long debounceInterval;
private long lastClickTime;
private View.OnClickListener clickListener;
public DebounceClickListener(final View.OnClickListener clickListener) {
this(clickListener, DEBOUNCE_INTERVAL_DEFAULT);
}
public DebounceClickListener(final View.OnClickListener clickListener, final long debounceInterval) {
this.clickListener = clickListener;
this.debounceInterval = debounceInterval;
}
@Override
public void onClick(final View v) {
if ((SystemClock.elapsedRealtime() - lastClickTime) < debounceInterval) {
return;
}
lastClickTime = SystemClock.elapsedRealtime();
clickListener.onClick(v);
}
}
Enjoy!
This is solved like this
Observable<Object> tapEventEmitter = _rxBus.toObserverable().share();
Observable<Object> debouncedEventEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS);
Observable<List<Object>> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEventEmitter);
debouncedBufferEmitter.buffer(debouncedEventEmitter)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<List<Object>>() {
@Override
public void call(List<Object> taps) {
_showTapCount(taps.size());
}
});