Androider
Androider

Reputation: 21335

Android Preventing Double Click On A Button

What is the best way to prevent double clicks on a button in Android?

Upvotes: 240

Views: 215842

Answers (30)

Yvgen
Yvgen

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

Manmohan
Manmohan

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

Afshin Samiei
Afshin Samiei

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

Lance Samaria
Lance Samaria

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

Parvez Alam
Parvez Alam

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

Felipe R. Saruhashi
Felipe R. Saruhashi

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

ali jabbari
ali jabbari

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

EVIL SnaKe
EVIL SnaKe

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

Akash Dubey
Akash Dubey

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

gustavo
gustavo

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

Mudassar
Mudassar

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

WindRider
WindRider

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

Hovanes Mosoyan
Hovanes Mosoyan

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

NickUnuchek
NickUnuchek

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

Nick Dandoulakis
Nick Dandoulakis

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

midblurz
midblurz

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

anna_manzhula
anna_manzhula

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

J&#233;w&#244;m&#39;
J&#233;w&#244;m&#39;

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

GianhTran
GianhTran

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

In kotlin

button.setOnClickListener { 
    it?.apply { isEnabled = false; postDelayed({ isEnabled = true }, 400) } //400 ms
    //do your work
}

Upvotes: 6

VinceMedi
VinceMedi

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

Milan Sheth
Milan Sheth

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

hamrosvet
hamrosvet

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

alexm
alexm

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

HaroldSer
HaroldSer

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

Jaydeep Khambhayta
Jaydeep Khambhayta

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

Salah Hammouda
Salah Hammouda

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

j2emanue
j2emanue

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

Spaceman Spiff
Spaceman Spiff

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

Priyavrat Talyan
Priyavrat Talyan

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

Related Questions