Impedimento do Android: clicar duas vezes em um botão


177

Qual é a melhor maneira de evitar cliques duplos em um botão no Android?


3
Veja a solução da qezt só que totalmente viável
Gennadiy Ryabkin

3
A solução do qezt deveria ter sido aceita, para que as pessoas que visitam esta página conheçam a desvantagem de usar setEnabled (false).
LoveForDroid 10/10

Você pode tentar minha biblioteca, usando a solução do último clique: github.com/RexLKW/SClick
Rex Lam

1
@Androider altere a sua melhor resposta
Amir133

Respostas:


86

Desative o botão com setEnabled(false)até que seja seguro para o usuário clicar nele novamente.


47
setEnabled (false) não parece funcionar "rápido" o suficiente. Eu configurei um View.OnClickListener.onClick (view) para um botão. A primeira coisa que faço em onClick () é escrever uma instrução de log, a segunda é button.setEnabled (false). Ao clicar duas vezes rapidamente no emulador, vejo a instrução log aparecer duas vezes! Mesmo ao adicionar um setClickable (false) logo depois, a instrução log aparece duas vezes.
QQQuestions

Hmmm, talvez o código até o setEnabled (true) no final de onClick () está sendo executado tão rápido que ele já está habilitado novamente ...
QQQuestions

91
Eu tive um problema parecido. De acordo com um cavalheiro útil, o Android realmente enfileira os cliques, por isso não importa a rapidez ou a lentidão do código onClick (); apenas a rapidez com que você clica no botão. Ou seja. Você já pode ter desabilitado o botão, mas a fila de cliques já foi preenchida com dois ou três cliques e desabilitar o botão não afeta a entrega dos cliques na fila. (embora talvez deva?) #
224 Nick #

3
Aquele raro momento em que você vê 'alguém' dá uma resposta exata e não abstrai respostas para a pergunta que está sendo feita ..: P
Rahul

essa nem sempre é a solução certa. Se sua operação onClick for cara, o botão não será desativado rápido o suficiente. A solução completa da IMHO é usar o horário do último clique, mas também desabilitar o botão para evitar que o usuário o toque novamente se você espera que a operação dure um tempo (como o login).
Sarang

368

salvar a última vez em que clicar ao evitar esse problema.

ie

private long mLastClickTime = 0;

...

// inside onCreate or so:

findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // mis-clicking prevention, using threshold of 1000 ms
        if (SystemClock.elapsedRealtime() - mLastClickTime < 1000){
            return;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        // do your magic here
    }
}

21
Esta é a única solução que realmente impede um clique duplo. Passei apenas uma hora tentando todos os outros, já que esse método é bastante desajeitado, mas nenhum deles poderia impedir um toque duplo rápido de uma exibição, exceto esta. Google, por favor, conserte isso :)
ashishduh

6
Esta solução funciona bem.Esta solução também funciona quando você tem vários botões na tela e deseja clicar apenas em um único botão de cada vez.
vazio

12
Isso não impede completamente o clique duplo, se você clicar duas vezes com rapidez suficiente.
Loc

4
Melhor resposta, ele deve estar no topo
Gennadiy Ryabkin

3
Esta solução deve ser a resposta certa ... setEnabled (false) não é suficiente.
heloisasim

55

Minha solução é

package com.shuai.view;

import android.os.SystemClock;
import android.view.View;

/**
 * 处理快速在某个控件上双击2次(或多次)会导致onClick被触发2次(或多次)的问题
 * 通过判断2次click事件的时间间隔进行过滤
 * 
 * 子类通过实现{@link #onSingleClick}响应click事件
 */
public abstract class OnSingleClickListener implements View.OnClickListener {
    /**
     * 最短click事件的时间间隔
     */
    private static final long MIN_CLICK_INTERVAL=600;
    /**
     * 上次click的时间
     */
    private long mLastClickTime;

    /**
     * click响应函数
     * @param v The view that was clicked.
     */
    public abstract void onSingleClick(View v);

    @Override
    public final void onClick(View v) {
        long currentClickTime=SystemClock.uptimeMillis();
        long elapsedTime=currentClickTime-mLastClickTime;
        //有可能2次连击,也有可能3连击,保证mLastClickTime记录的总是上次click的时间
        mLastClickTime=currentClickTime;

        if(elapsedTime<=MIN_CLICK_INTERVAL)
            return;

        onSingleClick(v);        
    }

}

O uso é semelhante ao OnClickListener, mas substitui onSingleClick ():

mTextView.setOnClickListener(new OnSingleClickListener() {
            @Override
            public void onSingleClick(View v) {
                if (DEBUG)
                    Log.i("TAG", "onclick!");
            }
     };

1
Fácil e perfeito para evitar automaticamente toques duplos.
Benjamin Piette 16/01

1
Isso não impede completamente o clique duplo, se você clicar duas vezes com rapidez suficiente.
Loc

1
Ótima solução. Acabei de alterar MIN_CLICK_INTERVAL = 1000;
N152 /

5
Se eu precisasse abotoar com precisão a cada 0,5 segundos, nenhum dos meus cliques subsequentes seria concluído porque o mLastClickTime seria atualizado a cada clique. Minha sugestão seria atribuir mLastClickTime depois de verificar o intervalo.
k2col

mLastClickTime = currentClickTime; deve ser colocado após if (elapsedTime <= MIN_CLICK_INTERVAL) retornar;
Loyea

41

Desabilitar o botão ou definir não clicável não é suficiente se você estiver fazendo um trabalho intensivo computacional em onClick (), pois os eventos de clique podem ficar na fila antes que o botão possa ser desabilitado. Eu escrevi uma classe base abstrata que implementa o OnClickListener que você pode substituir, que corrige esse problema ignorando todos os cliques na fila:

/** 
 * This class allows a single click and prevents multiple clicks on
 * the same button in rapid succession. Setting unclickable is not enough
 * because click events may still be queued up.
 * 
 * Override onOneClick() to handle single clicks. Call reset() when you want to
 * accept another click.
 */
public abstract class OnOneOffClickListener implements OnClickListener {
    private boolean clickable = true;

    /**
     * Override onOneClick() instead.
     */
    @Override
    public final void onClick(View v) {
        if (clickable) {
            clickable = false;
            onOneClick(v);
            //reset(); // uncomment this line to reset automatically
        }
    }

    /**
     * Override this function to handle clicks.
     * reset() must be called after each click for this function to be called
     * again.
     * @param v
     */
    public abstract void onOneClick(View v);

    /**
     * Allows another click.
     */
    public void reset() {
        clickable = true;
    }
}

O uso é o mesmo que OnClickListener, mas substitui OnOneClick ():

OnOneOffClickListener clickListener = new OnOneOffClickListener() {
    @Override
    public void onOneClick(View v) {

        // Do stuff

        this.reset(); // or you can reset somewhere else with clickListener.reset();
    }
};
myButton.setOnClickListener(clickListener);

Obrigado pelo código. Com base no que você escreveu aqui, criei uma classe semelhante que evita cliques se o botão ou o pai estiver oculto - é engraçado que o Android enfileire os cliques, mesmo que você oculte o botão imediatamente após o clique.
Nikib3ro

1
Eu tenho uma solução semelhante - você pode simplesmente usar esta classe em xml e ela envolverá os clickListeners para você github.com/doridori/AndroidUtilDump/blob/master/android/src/…
Dori

2
Esta é uma boa solução, mas onClick () e reset () precisam ser sincronizados, caso contrário, um duplo-clique ainda pode esgueirar-se.
Tom anMoney

6
@TomanMoney, como? Todos os eventos de clique não acontecem em um único thread da interface do usuário?
Ken

@Dori o link está morto
NAXA

36

Você pode fazer isso de maneira muito sofisticada com as Funções de extensão Kotlin e RxBinding

   fun View.clickWithDebounce(debounceTime: Long = 600L, action: () -> Unit): Disposable =
        RxView.clicks(this)
                .debounce(debounceTime, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { action() }

ou

fun View.clickWithDebounce(debounceTime: Long = 600L, action: () -> Unit) {
    this.setOnClickListener(object : View.OnClickListener {
        private var lastClickTime: Long = 0

        override fun onClick(v: View) {
            if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
            else action()

            lastClickTime = SystemClock.elapsedRealtime()
        }
    })
}

e então apenas:

View.clickWithDebounce{ Your code }

sobre engenharia
Serg Burlaka 9/10

3
O debounce para Rx não funcionará bem aqui. Em vez disso, "ThrottleFirst" deve se encaixar melhor. demo.nimius.net/debounce_throttle aqui é um exemplo de como eles diferem. PS: e realmente sua implementação clickWithDebounce é a implementação do acelerador, não debounce
krossovochkin

13

Eu também corro em um problema semelhante, eu estava exibindo alguns marcadores de data e hora em que às vezes recebia 2 vezes. Eu resolvi isso com isso

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

Você pode alterar o tempo, dependendo da sua exigência. Este método funciona para mim.


Esta é a solução real, pois reativa o botão automaticamente após o tempo decorrido. As outras soluções bloqueiam o botão permanentemente se você clicar duas vezes rapidamente. Esta solução corrige o problema! Obrigado
Néstor

10

setEnabled(false) funciona perfeitamente para mim.

A ideia é que eu escreva { setEnabled(true); }no começo e apenas faça isso falseno primeiro clique do botão.


9

Sei que é uma pergunta antiga, mas compartilho a melhor solução que encontrei para resolver esse problema comum

        btnSomeButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // Prevent Two Click
            Utils.preventTwoClick(view);
            // Do magic
        }
    });

E em outro arquivo, como 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(new Runnable() {
        public void run() {
           view.setEnabled(true);
        }
    }, 500);
}

8

A solução real para esse problema é usar setEnabled (false), que pressiona o botão, e setClickable (false), que faz com que o segundo clique não possa ser recebido. Eu testei isso e parece ser muito eficaz.


7

Se alguém ainda estiver procurando uma resposta curta, use o código abaixo

 private static long mLastClickTime = 0;
  if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) { // 1000 = 1second
         return;
    }
 mLastClickTime = SystemClock.elapsedRealtime();

Este código entrará if statementsempre que o usuário clicar no View within 1 seconde, em seguida return;, será iniciado e o código adicional não será iniciado.


2
Essa deve ser a resposta aceita. Super simples e funciona mesmo se você clicar muito rápido! Obrigado por isso.
Geoffroy CALA

7

A maneira idiomática do K LEANEST Kotlin :

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

Uso:

button.setOnSingleClickListener { ... }

6

Minha solução é tentar usar uma booleanvariável:

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

E usando como abaixo:

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

ATUALIZAÇÃO : solução Kotlin, usando a extensão de exibição

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

5

Click Guard funciona bem com a faca de manteiga

ClickGuard.guard(mPlayButton);

4
separar linrary para manipular clique duas vezes no botão? ha
Serg Burlaka 9/10

Posso usar este botão personalizado interno?
Ara Badalyan

@AraBadalyan, o que você quer dizer com 'dentro'. Basta passar seu botão personalizado como parâmetro para o método de guarda. Em seguida, seu botão estará protegido contra clique duplo
thanhbinh84

Quero dizer, não quero adicionar ClickGuard.guard (mPlayButton) em todas as atividades que usam o onclick. Quero criar CustomButton extensão Button e colocar ClickGuard dentro CustomButton. Mas quando adiciono o ClickGuard no construtor CustomButton, ele não funcionou.
Ara Badalyan 27/12/19

@AraBadalyan isso mesmo. Não é assim que funciona.
thanhbinh84

5

na minha situação, eu estava usando a exibição de botão e os cliques eram rápidos demais. basta desativar o clicável e ativá-lo novamente depois de alguns segundos ...

Basicamente, eu criei uma classe de wrapper que envolve seus Views onClickListener. você também pode definir um atraso personalizado, se desejar.

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

e para chamá-lo simplesmente faça o seguinte:

mMyButton.setOnClickListener(new OnClickRateLimitedDecoratedListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 doSomething();
             }
         }));

ou forneça seu próprio atraso:

 mMyButton.setOnClickListener(new OnClickRateLimitedDecoratedListener(new View.OnClickListener() {
                     @Override
                     public void onClick(View v) {
                         doSomething();
                     }
                 },1000));

ATUALIZAÇÃO: Acima das maneiras um pouco antiquadas, agora que o RxJava é tão prevalente. Como outros já mencionaram, no Android, podemos usar um acelerador para diminuir a velocidade dos cliques. aqui está um exemplo:

 RxView.clicks(myButton)
                    .throttleFirst(2000, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
                    .subscribe {
                        Log.d("i got delayed clicked")
                    }
        }

você pode usar esta biblioteca para isso: implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'


4
    button.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            //to prevent double click
            button.setOnClickListener(null);
        }
    });

4

Você pode usar esse método Ao usar o atraso de postagem, você pode cuidar de eventos de clique duplo.

void debounceEffectForClick (exibição de exibição) {

    view.setClickable(false);

    view.postDelayed(new Runnable() {
        @Override
        public void run() {
            view.setClickable(true);

        }
    }, 500);
}

4

Extensão Kotlin que permite código embutido conciso e tempos de espera variáveis ​​com clique duplo

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

Uso:

anyView.setNoDoubleClickListener(View.OnClickListener { v ->
    // do stuff
})

Ou

anyView.setNoDoubleClickListener(View.OnClickListener { v ->
    // do stuff
}, 1500)

Impressionante, todos devem usar isso, seu excelente código e a grande razão pela qual todos devem migrar para o kotlin.
Achmad Naufal Syafiq 14/01/19

Pode ser melhor se o waitMillis for apenas codificado, então os parênteses externos podem ser eliminados.
WindRider 16/09/19

4

O código abaixo evitará que o usuário clique várias vezes em frações de segundos e permita apenas após 3 segundos.

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();
    }
}

3

Parece que definir os ouvintes de clique em onResume e anulá-los em onPause também funciona.


3

Para mim, apenas lembrar o carimbo de data e hora e checá-lo (que mais de 1 segundo se passou desde o clique anterior) ajudaram.


3

Espero que isso possa ajudá-lo, coloque o código no seu manipulador de eventos.

// ------------------------------------------------ --------------------------------

    boolean hasTag = null != which.getTag( R.id.preventing_double_click_tag );

    if ( hasTag ) {
        // Do not handle again...
        return;
    } else {
        which.setTag( R.id.action, Boolean.TRUE );

        which.postDelayed( new Runnable() {
            @Override
            public void run() {
                which.setTag( R.id.action, null );
                Log.d( "onActin", " The preventing double click tag was removed." );
            }

        }, 2000 );
    }

3

Descobri que nenhuma dessas sugestões funciona se o método onClick não retornar imediatamente. O evento de toque é enfileirado pelo Android e o próximo onClick é chamado somente após a conclusão do primeiro. (Como isso é feito em um thread da interface do usuário, isso é realmente normal.) Eu precisava usar o horário em que a função onClick foi concluída + uma variável booleana para marcar se o onClick fornecido está em execução. Ambos os atributos do marcador são estáticos para evitar que onClickListener seja executado ao mesmo tempo. (Se o usuário clicar em outro botão) Você pode substituir seu OnClickListener por essa classe e, em vez de implementar o método onClick, é necessário implementar o método abstract oneClick ().

    abstract public class OneClickListener implements OnClickListener {

    private static boolean started = false;
    private static long lastClickEndTime = 0;

    /* (non-Javadoc)
     * @see android.view.View.OnClickListener#onClick(android.view.View)
     */
    @Override
    final public void onClick(final View v) {
        if(started || SystemClock.elapsedRealtime()-lastClickEndTime <1000 ){
            Log.d(OneClickListener.class.toString(), "Rejected double click, " + new Date().toString() );
            return; 
        }
        Log.d(OneClickListener.class.toString(), "One click, start: " + new Date().toString() );
        try{
            started = true;
            oneClick(v);
        }finally{
            started = false;
            lastClickEndTime = SystemClock.elapsedRealtime();
            Log.d(OneClickListener.class.toString(), "One click, end: " + new Date().toString() );
        }
    }

    abstract protected void oneClick(View v);
}

3

você também pode usar ligações rx por jake wharton para fazer isso. Aqui está um exemplo que preenche 2 segundos entre cliques sucessivos:

RxView.clicks(btnSave)
                .throttleFirst(2000, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Object>() {
                    @Override
                    public void accept( Object v) throws Exception {
//handle onclick event here
                });

// note: ignore o objeto v nesse caso e acho que sempre.


3

Kotlin cria a classe 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)
    }
}

crie uma função em baseClass ou então

fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) {val safeClickListener = SafeClickListener {
        onSafeClick(it)
    }
    setOnClickListener(safeClickListener)
}

e use no botão, clique

btnSubmit.setSafeOnClickListener {
    showSettingsScreen()
}

1
A solução mais limpa com extensões! Obrigado!
android_dev 25/10/19



2

Definir Clicável como falso não funciona no primeiro clique duplo, mas os cliques duplos subsequentes são bloqueados. É como se o clique de carregamento delegado fosse mais lento na primeira vez e o segundo clique fosse capturado antes da conclusão do primeiro.

        Button button = contentView.FindViewById<Button>(Resource.Id.buttonIssue);
        button.Clickable = false;
        IssueSelectedItems();
        button.Clickable = true;

2

Corrijo esse problema usando dois clases, um semelhante ao da resposta @ jinshiyi11 e o anoter é baseado em cliques explícitos; neste, você pode clicar em um botão apenas uma vez, se desejar outro clique, deve indicar explicitamente .

/**
 * Listener que sólo permite hacer click una vez, para poder hacer click
 * posteriormente se necesita indicar explicitamente.
 *
 * @author iberck
 */
public abstract class OnExplicitClickListener implements View.OnClickListener {

    // you can perform a click only once time
    private boolean canClick = true;

    @Override
    public synchronized void onClick(View v) {
        if (canClick) {
            canClick = false;
            onOneClick(v);
        }
    }

    public abstract void onOneClick(View v);

    public synchronized void enableClick() {
        canClick = true;
    }

    public synchronized void disableClick() {
        canClick = false;
    }
}

Exemplo de uso:

OnExplicitClickListener clickListener = new OnExplicitClickListener() {
    public void onOneClick(View v) {
        Log.d("example", "explicit click");
        ...
        clickListener.enableClick();    
    }
}
button.setOnClickListener(clickListener);

2
final Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {

    private final AtomicBoolean onClickEnabled = new AtomicBoolean(true);

    @Override
    public void onClick(View v) {
        Log.i("TAG", "onClick begin");
        if (!onClickEnabled.compareAndSet(true, false)) {
            Log.i("TAG", "onClick not enabled");
            return;
        }

        button.setEnabled(false);

        // your action here

        button.setEnabled(true);
        onClickEnabled.set(true);
        Log.i("TAG", "onClick end");
    }
});

Os ouvintes de exibição são sempre chamados em um único thread (principal), portanto, o uso AtomicBooleannão pode ajudar.
WindRider 16/09/19

2

Tente isso, está funcionando:

mButton.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {

                mSlotLayout.setEnabled(false);

        //      do your work here

                Timer buttonTimer = new Timer();
                buttonTimer.schedule(new TimerTask() {

                    @Override
                    public void run() {

                        runOnUiThread(new Runnable() {

                            @Override
                            public void run() {
                                mButton.setEnabled(true);
                            }
                        });
                    }
                }, 500); // delay button enable for 0.5 sec
    }
});
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.