Reputation: 21335
What is the best way to prevent double clicks on a button in Android?
Upvotes: 240
Views: 215842
Reputation: 2212
You can do it in very fancy way with Kotlin Extension Functions and RxBinding
fun View.clickWithThrottle(throttleTime: Long = 600L, action: () -> Unit): Disposable =
RxView.clicks(this)
.throttleFirst(throttleTime, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { action() }
or
fun View.clickWithThrottle(throttleTime: Long = 600L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < throttleTime) return
else action()
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
and then just:
View.clickWithThrottle{ Your code }
UPD. Code for compose:
inline fun Modifier.debounceClickable(
debounceInterval: Long = 600,
crossinline onClick: () -> Unit,
): Modifier = composed {
var lastClickTime by remember { mutableStateOf(0L) }
clickable {
val currentTime = System.currentTimeMillis()
if ((currentTime - lastClickTime) < debounceInterval) return@clickable
lastClickTime = currentTime
onClick()
}
}
Upvotes: 46
Reputation: 440
You can use this method. By using post delay you can take care for double click events.
void debounceEffectForClick(View view) {
view.setClickable(false);
view.postDelayed(new Runnable() {
@Override
public void run() {
view.setClickable(true);
}
}, 500);
}
Upvotes: 5
Reputation: 129
You can use the following method instead of setOnclickListener in your entire project to avoid multiple clicks on views:
fun View.setDelayedClick(delay : Long,onClickListener: OnClickListener){
this.setOnClickListener {
this.isEnabled = false
CoroutineScope(Dispatchers.Main).launch {
delay(delay)
onClickListener.onClick(it)
[email protected] = true
}
}
}
and this is the way to use it:
yourview.setDelayedClick(200){
// onClick triggered
}
}
simple!
Upvotes: 0
Reputation: 19622
Coming from the iOS world I use a Timer, when the user presses the button I check if it's initialized, if so then return, if not I initialize the timer.
In the Android world I use a Job
.
Gradle.app:
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$2.40"
In the Activity with the button:
private var job: Job? = null
private fun avoidDoubleTap() {
cancelJob()
job = lifecycleScope.launch {
ensureActive()
delay(500L)
cancelJob()
}
}
private fun cancelJob() {
job?.cancel()
job = null
}
fun postButtonTapped() {
if (job != null) { return }
avoidDoubleTap()
println("... continue on")
}
override fun onStop() {
super.onStop()
cancelJob()
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
binding.postButton.setOnClickListener {
postButtonTapped()
}
}
Upvotes: 0
Reputation: 31
This answer works no need to define it in every class It helps to prevent multiple click on single button
Create An Extension function
class OnSingleClickListener(private val click: (View) -> Unit):View.OnClickListener
{
private var lastClickTime = 0L
override fun onClick(view: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < 1200) {
return
}
lastClickTime = SystemClock.elapsedRealtime()
click(view)
}
}
fun View.setOnSingleClickListener(block: (View) -> Unit) {
setOnClickListener(OnSingleClickListener(block))
}
use with:
Buttonclick.setOnSingleClickListener{view->
}
Upvotes: 3
Reputation: 1737
Combining the Gustavo answer, I've implemented in Kotlin with extensions that looks pretty clean:
fun View.setOnSingleClickListener(l: () -> Unit) {
setOnClickListener { view ->
l.invoke(view)
isEnabled = false
postDelayed({
isEnabled = true
}, 600)
}
}
This is how would you set this single click listener:
yourView.setOnSingleClickListener {
// Insert your code here
}
Upvotes: 1
Reputation: 61
With Kotlin extension function :
fun View.onSingleClick(action: (v: View) -> Unit) {
setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
isClickable = false
action(v)
postDelayed({ isClickable = true }, 700)
}
})
}
usage:
button.onSingleClick { myAction() }
Upvotes: 6
Reputation: 335
Kotlin approach with extension:
fun View.setOneTimeClickListener(delayMillis: Long = 1000, block: () -> Unit) {
setOnClickListener {
this.isEnabled = false
block()
postDelayed({ isEnabled = true }, delayMillis)
}
Usage in code:
someView.setOneTimeClickListener { someFun() }
The delayMillis argument can be used to set the amount of time the button will be disabled.
someView.setOneTimeClickListener(500) { someFun() }
Upvotes: 2
Reputation: 1548
Below is the kotlin way with extension function that will work for all views, keep below function in ur Utils or any File
fun View.preventDoubleClick() {
this.isEnabled = false
this.postDelayed( { this.isEnabled = true }, 1000)
}
Below is how to use it from fragment or activity
anyIdOfView?.setOnClickListener {
it.preventDoubleClick()
YourAction()
}
Upvotes: 3
Reputation: 178
I know it's an old question, but I share the best solution I found to solve this common problem
btnSomeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Prevent Two Click
Utils.preventTwoClick(view);
// Do magic
}
});
And in another file,like Utils.java
/**
* Método para prevenir doble click en un elemento
* @param view
*/
public static void preventTwoClick(final View view){
view.setEnabled(false);
view.postDelayed(
()-> view.setEnabled(true),
500
);
}
Upvotes: 17
Reputation: 1576
I also ran into a similar problem, I was displaying some date pickers & time pickers where sometimes it got clicked 2 times. I have solved it with this:
long TIME = 1 * 1000;
@Override
public void onClick(final View v) {
v.setEnabled(false);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
v.setEnabled(true);
}
}, TIME);
}
You can change time depending upon your requirement.
Upvotes: 18
Reputation: 12318
The KLEANEST Kotlin idiomatic way:
class OnSingleClickListener(private val block: () -> Unit) : View.OnClickListener {
private var lastClickTime = 0L
override fun onClick(view: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < 1000) {
return
}
lastClickTime = SystemClock.elapsedRealtime()
block()
}
}
fun View.setOnSingleClickListener(block: () -> Unit) {
setOnClickListener(OnSingleClickListener(block))
}
Usage:
button.setOnSingleClickListener { ... }
Or with an added parameter for controlling the throttle
class OnClickListenerThrottled(private val block: () -> Unit, private val wait: Long) : View.OnClickListener {
private var lastClickTime = 0L
override fun onClick(view: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < wait) {
return
}
lastClickTime = SystemClock.elapsedRealtime()
block()
}
}
/**
* A throttled click listener that only invokes [block] at most once per every [wait] milliseconds.
*/
fun View.setOnClickListenerThrottled(wait: Long = 1000L, block: () -> Unit) {
setOnClickListener(OnClickListenerThrottled(block, wait))
}
Usages:
button.setOnClickListenerThrottled(2000L) { /** some action */}
or
button.setOnClickListenerThrottled { /** some action */}
Upvotes: 35
Reputation: 759
We could use the button just synchronized like:
Example #1 (Java)
@Override
public void onClick(final View view) {
synchronized (view) {
view.setEnabled(false);
switch (view.getId()) {
case R.id.id1:
...
break;
case R.id.id2:
...
break;
...
}
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
view.setEnabled(true);
}
}, 1000);
}
}
Example #2 (kotlin) using synchronized
myButton.setOnClickListener { view ->
synchronized(view) {
view.isEnabled = false
// do something
view.postDelayed({ view.isEnabled = true }, 500L)
}
}
Good Luck)
Upvotes: 6
Reputation: 12887
Prevents click on multiply btns
Using:
private val disposables = CompositeDisposable()
private val clickInteractor = ClickInteractor(disposables)
...
button1.setOnClickListener{
clickInteractor.click {
Toast.makeText(context, "Btn1", Toast.LENGTH_LONG).show()
}
}
button2.setOnClickListener{
clickInteractor.click {
Toast.makeText(context, "Btn2", Toast.LENGTH_LONG).show()
}
}
ClickInteractor.kt:
class ClickInteractor constructor(disposables: CompositeDisposable) {
private val performPublish = PublishSubject.create<ClickInteractorCallback>()
init {
performPublish
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.throttleFirst(1, TimeUnit.SECONDS, Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { callback ->
callback.invoke()
}
.retry()
.execute(disposables)
}
fun click(callback: ClickInteractorCallback) {
performPublish.onNext(callback)
}
}
typealias ClickInteractorCallback = () -> Unit
Upvotes: 0
Reputation: 43140
Here's a OnClickListener
proxy that prevents successive clicks, based on qezt's answer.
import android.os.SystemClock;
import android.view.View;
public class MultiClickGuard implements View.OnClickListener {
private long mLastClickTime;
private final int mThresholdMillis;
private final View.OnClickListener mListener;
public MultiClickGuard(View.OnClickListener listener, int thresholdMillis) {
if (listener == null) {
throw new NullPointerException("Null OnClickListener");
}
if (thresholdMillis < 0) {
throw new IllegalArgumentException("Negative click threshold: " + thresholdMillis);
}
mListener = listener;
mThresholdMillis = thresholdMillis;
}
@Override
public void onClick(View v) {
// Using a time threshold to prevent successive clicks.
if (SystemClock.elapsedRealtime() - mLastClickTime < mThresholdMillis) {
return;
}
mLastClickTime = SystemClock.elapsedRealtime();
// Forward the click event to the *real* listener.
mListener.onClick(v);
}
}
Usage examples
button.setOnClickListener(new MultiClickGuard(new View.OnClickListener() {
@Override
public void onClick(View v) {
// do something
}
}, 1000));
button.setOnClickListener(new MultiClickGuard(v -> {...}, 1000));
button.setOnClickListener(new MultiClickGuard(v -> doSomething(), 1000));
In situations where you're trying to prevent starting multiple instances of an activity, consider specifying the launch mode: Understand Tasks and Back Stack, which is the reliable way to do it.
If you're trying to prevent opening multiple instances of a dialog fragment, you can check if the fragment manager already contains the dialog, e.g. getSupportFragmentManager().findFragmentByTag(tag)
.
Upvotes: 0
Reputation: 27
This solution (Kotlin) works on me:
abstract class SingleClickListener : View.OnClickListener {
private val MIN_CLICK_INTERVAL: Long = 1000
private var mLastClickTime: Long = 0
abstract fun onSingleClick(v: View?)
override fun onClick(v: View?) {
if (mLastClickTime <= 0) {
mLastClickTime = SystemClock.uptimeMillis()
onSingleClick(v)
return
}
if (SystemClock.uptimeMillis() - mLastClickTime <= MIN_CLICK_INTERVAL) {
return
}
mLastClickTime = SystemClock.uptimeMillis()
onSingleClick(v)
}
}
Usage:
someView.setOnClickListener(object : SingleClickListener() {
override fun onSingleClick(v: View?) {
v?.also { klik(it) }
}
})
Or also create extension function for adding ClickListener on view easily:
fun View.click(klik: (View) -> Unit) {
this.setOnClickListener(object : SingleClickListener() {
override fun onSingleClick(v: View?) {
v?.also { klik(it) }
}
})
}
// Usage
class XPerimentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_xperiment_layout)
submit_button.click {
// do your magic
}
}
}
Upvotes: 1
Reputation: 324
There is a native debounce click listener in Java
view.setOnClickListener(new DebouncedOnClickListener(1000) { //in milisecs
@Override
public void onDebouncedClick(View v) {
//action
}
});
Upvotes: 0
Reputation: 3971
Try this Kotlin extension function :
private var lastClickTime = 0L
fun View.click(action: () -> Unit) {
setOnClickListener {
if (SystemClock.elapsedRealtime() - lastClickTime < 600L)
return@setOnClickListener
lastClickTime = SystemClock.elapsedRealtime()
action()
}
}
It prevent also clicking in various parts of the app at the same time.
Upvotes: 0
Reputation: 3711
My solution is try to using a boolean
variable :
public class Blocker {
private static final int DEFAULT_BLOCK_TIME = 1000;
private boolean mIsBlockClick;
/**
* Block any event occurs in 1000 millisecond to prevent spam action
* @return false if not in block state, otherwise return true.
*/
public boolean block(int blockInMillis) {
if (!mIsBlockClick) {
mIsBlockClick = true;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mIsBlockClick = false;
}
}, blockInMillis);
return false;
}
return true;
}
public boolean block() {
return block(DEFAULT_BLOCK_TIME);
}
}
And using as below:
view.setOnClickListener(new View.OnClickListener() {
private Blocker mBlocker = new Blocker();
@Override
public void onClick(View v) {
if (!mBlocker.block(block-Time-In-Millis)) {
// do your action
}
}
});
UPDATE: Kotlin solution, using view extension
fun View.safeClick(listener: View.OnClickListener, blockInMillis: Long = 500) {
var lastClickTime: Long = 0
this.setOnClickListener {
if (SystemClock.elapsedRealtime() - lastClickTime < blockInMillis) return@setOnClickListener
lastClickTime = SystemClock.elapsedRealtime()
listener.onClick(this)
}
}
Upvotes: 8
Reputation: 154
In kotlin
button.setOnClickListener {
it?.apply { isEnabled = false; postDelayed({ isEnabled = true }, 400) } //400 ms
//do your work
}
Upvotes: 6
Reputation: 210
My solution (Kotlin):
class OnDebouncedClickListener(private val delayInMilliSeconds: Long, val action: () -> Unit) : View.OnClickListener {
var enable = true
override fun onClick(view: View?) {
if (enable) {
enable = false
view?.postDelayed(delayInMilliSeconds) { enable = true }
action()
}
}
}
fun View.setOnDebouncedClickListener(delayInMilliSeconds: Long = 500, action: () -> Unit) {
val onDebouncedClickListener = OnDebouncedClickListener(delayInMilliSeconds, action)
setOnClickListener(onDebouncedClickListener)
}
Use :
button.apply {
setOnDebouncedClickListener {
//your action on click
}
}
Upvotes: 5
Reputation: 972
Below code will prevent user to click multiple times within a fractions of seconds and allow only after 3 seconds.
private long lastClickTime = 0;
View.OnClickListener buttonHandler = new View.OnClickListener() {
public void onClick(View v) {
// preventing double, using threshold of 3000 ms
if (SystemClock.elapsedRealtime() - lastClickTime < 3000){
return;
}
lastClickTime = SystemClock.elapsedRealtime();
}
}
Upvotes: 6
Reputation: 1198
Adding to Jim's answer the code can be made more concise:
fun View.setOnSingleClick(onClick: () -> Unit) {
var lastClickTime = 0L
setOnClickListener {
if (currentTimeMillis() > lastClickTime + 750) onClick()
lastClickTime = currentTimeMillis()
}
}
Usage:
aView.setOnSingleClick { }
Upvotes: 3
Reputation: 1315
This solution is quick and neat.
basically you just prevent double touch from the base styles in your app and implement standard on click listener. This works like a charm with touches at the same time of different views.
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
...
<item name="android:splitMotionEvents">false</item>
<item name="android:windowEnableSplitTouch">false</item>
</style>
If your app uses some kind of gesture event this is not the right answer.
Upvotes: 1
Reputation: 2065
Setting the button as clickable false upon clicking and true once it is desired to make the button clickable again is the right approach. For instance, consider the following scenario: you are making a service call upon click of a button and once the service is done you want to display a dialog. For this, once the button is clicked you can set setClickable(false) and once the service responds you will do setClicklable(true) through a reference you pass to your custom dialog. When dialog invokes isShowing() you can trigger the listener and setClicklable(true).
Upvotes: 0
Reputation: 5339
Kotlin create class SafeClickListener
class SafeClickListener(
private var defaultInterval: Int = 1000,
private val onSafeCLick: (View) -> Unit
) : View.OnClickListener {
private var lastTimeClicked: Long = 0 override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
return
}
lastTimeClicked = SystemClock.elapsedRealtime()
onSafeCLick(v)
}
}
create a function in baseClass or else
fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) {val safeClickListener = SafeClickListener {
onSafeClick(it)
}
setOnClickListener(safeClickListener)
}
and use on button click
btnSubmit.setSafeOnClickListener {
showSettingsScreen()
}
Upvotes: 5
Reputation: 303
for any one using data-binding :
@BindingAdapter("onClickWithDebounce")
fun onClickWithDebounce(view: View, listener: android.view.View.OnClickListener) {
view.setClickWithDebounce {
listener.onClick(view)
}
}
object LastClickTimeSingleton {
var lastClickTime: Long = 0
}
fun View.setClickWithDebounce(action: () -> Unit) {
setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - LastClickTimeSingleton.lastClickTime < 500L) return
else action()
LastClickTimeSingleton.lastClickTime = SystemClock.elapsedRealtime()
}
})
}
<androidx.appcompat.widget.AppCompatButton
..
android:text="@string/signup_signin"
app:onClickWithDebounce="@{() -> viewModel.onSignUpClicked()}"
... />
Upvotes: 1
Reputation: 62549
in my situation i was using a button view and it was taking the clicks too quickly. just disable the clickable and enable it again after a few seconds...
Basically i made a wrapper class that wraps around your Views onClickListener. you can also set a custom delay if you want.
public class OnClickRateLimitedDecoratedListener implements View.OnClickListener {
private final static int CLICK_DELAY_DEFAULT = 300;
private View.OnClickListener onClickListener;
private int mClickDelay;
public OnClickRateLimitedDecoratedListener(View.OnClickListener onClickListener) {
this(onClickListener, CLICK_DELAY_DEFAULT);
}
//customize your own delay
public OnClickRateLimitedDecoratedListener(View.OnClickListener onClickListener, int delay) {
this.onClickListener = onClickListener;
mClickDelay = delay;
}
@Override
public void onClick(final View v) {
v.setClickable(false);
onClickListener.onClick(v);
v.postDelayed(new Runnable() {
@Override
public void run() {
v.setClickable(true);
}
}, mClickDelay);
}
}
and to call it simply do this:
mMyButton.setOnClickListener(new OnClickRateLimitedDecoratedListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
}));
or provide your own delay:
mMyButton.setOnClickListener(new OnClickRateLimitedDecoratedListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
},1000));
UPDATE: Above ways a little old fashion now that RxJava is so prevalent. as others have mentioned, in android we could use a throttle to slow down the clicks. here is one example:
RxView.clicks(myButton)
.throttleFirst(2000, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe {
Log.d("i got delayed clicked")
}
}
you can use this library for it: implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
Upvotes: 5
Reputation: 731
Kotlin extension that allows for concise inline code & variable double click wait times
fun View.setDoubleClickListener(listener: View.OnClickListener, waitMillis : Long = 1000) {
var lastClickTime = 0L
setOnClickListener { view ->
if (System.currentTimeMillis() > lastClickTime + waitMillis) {
listener.onClick(view)
lastClickTime = System.currentTimeMillis()
}
}
}
Usage:
anyView.setNoDoubleClickListener(View.OnClickListener { v ->
// do stuff
})
Or
anyView.setNoDoubleClickListener(View.OnClickListener { v ->
// do stuff
}, 1500)
Upvotes: 4
Reputation: 121
The Best and simple solution i found is
1. to create a boolean and set as false (default) like
private boolean itemClicked = false;
/* for a safer side you can also declare boolean false in onCreate() also. */
and at onclick() method check
2. if(!itemClicked)
{
itemClicked = true;
// rest of your coding functionality goes here of onClick method.
}
3. last step is to set boolean false in onResume()
@override
onResume()
{
super.onResume(0);
itemClicked = false;
}
Upvotes: 0