Eu tenho um problema com meu aplicativo que, se o usuário clicar no botão várias vezes rapidamente, vários eventos serão gerados antes mesmo de minha caixa de diálogo segurando o botão desaparecer
Eu conheço uma solução definindo uma variável booleana como um sinalizador quando um botão é clicado para que futuros cliques possam ser evitados até que a caixa de diálogo seja fechada. No entanto, tenho muitos botões e ter que fazer isso sempre para todos os botões parece ser um exagero. Não há outra maneira no Android (ou talvez alguma solução mais inteligente) para permitir apenas a ação de evento gerada por clique de botão?
O que é ainda pior é que vários cliques rápidos parecem gerar várias ações de evento antes mesmo que a primeira ação seja tratada, portanto, se eu quiser desabilitar o botão no método de tratamento do primeiro clique, já existem ações de eventos existentes na fila esperando para serem tratadas!
Por favor ajude obrigado
Respostas:
Aqui está um listener onClick 'debelado' que escrevi recentemente. Você informa qual é o número mínimo aceitável de milissegundos entre os cliques. Implemente sua lógica em
onDebouncedClick
vez deonClick
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); } } }
fonte
Com RxBinding, isso pode ser feito facilmente. Aqui está um exemplo:
RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> { // action on click });
Adicione a seguinte linha
build.gradle
para adicionar a dependência RxBinding:compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
fonte
Aqui está minha versão da resposta aceita. É muito semelhante, mas não tenta armazenar visualizações em um mapa, o que não acho uma boa ideia. Ele também adiciona um método de agrupamento que pode ser útil em muitas situações.
/** * Implementation of {@link OnClickListener} that ignores subsequent clicks that happen too quickly after the first one.<br/> * To use this class, implement {@link #onSingleClick(View)} instead of {@link OnClickListener#onClick(View)}. */ public abstract class OnSingleClickListener implements OnClickListener { private static final String TAG = OnSingleClickListener.class.getSimpleName(); private static final long MIN_DELAY_MS = 500; private long mLastClickTime; @Override public final void onClick(View v) { long lastClickTime = mLastClickTime; long now = System.currentTimeMillis(); mLastClickTime = now; if (now - lastClickTime < MIN_DELAY_MS) { // Too fast: ignore if (Config.LOGD) Log.d(TAG, "onClick Clicked too quickly: ignored"); } else { // Register the click onSingleClick(v); } } /** * Called when a view has been clicked. * * @param v The view that was clicked. */ public abstract void onSingleClick(View v); /** * Wraps an {@link OnClickListener} into an {@link OnSingleClickListener}.<br/> * The argument's {@link OnClickListener#onClick(View)} method will be called when a single click is registered. * * @param onClickListener The listener to wrap. * @return the wrapped listener. */ public static OnClickListener wrap(final OnClickListener onClickListener) { return new OnSingleClickListener() { @Override public void onSingleClick(View v) { onClickListener.onClick(v); } }; } }
fonte
myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
Podemos fazer isso sem nenhuma biblioteca. Basta criar uma única função de extensão:
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() } }) }
Visualize onClick usando o código abaixo:
buttonShare.clickWithDebounce { // Do anything you want }
fonte
Você pode usar este projeto: https://github.com/fengdai/clickguard para resolver esse problema com uma única declaração:
ClickGuard.guard(button);
ATUALIZAÇÃO: Esta biblioteca não é mais recomendada. Prefiro a solução da Nikita. Em vez disso, use RxBinding.
fonte
ListView
Aqui está um exemplo simples:
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); }
fonte
Apenas uma atualização rápida sobre a solução GreyBeardedGeek. Altere a cláusula if e adicione a função Math.abs. Defina assim:
if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) { onDebouncedClick(clickedView); }
O usuário pode alterar a hora no dispositivo Android e colocá-la no passado, portanto, sem isso, pode levar a um bug.
PS: não tenho pontos suficientes para comentar sua solução, então acabei de colocar outra resposta.
fonte
Aqui está algo que funcionará com qualquer evento, não apenas com cliques. Ele também entregará o último evento, mesmo que faça parte de uma série de eventos rápidos (como 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) Construa o Debouncer em um campo do ouvinte, mas fora da função de retorno de chamada, configurado com tempo limite e o retorno de chamada fn que você deseja controlar.
2) Chame o método de spam do Debouncer na função de retorno de chamada do ouvinte.
fonte
Solução semelhante usando RxJava
import android.view.View; import java.util.concurrent.TimeUnit; import rx.android.schedulers.AndroidSchedulers; import rx.functions.Action1; import rx.subjects.PublishSubject; public abstract class SingleClickListener implements View.OnClickListener { private static final long THRESHOLD_MILLIS = 600L; private final PublishSubject<View> viewPublishSubject = PublishSubject.<View>create(); public SingleClickListener() { viewPublishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<View>() { @Override public void call(View view) { onClicked(view); } }); } @Override public void onClick(View v) { viewPublishSubject.onNext(v); } public abstract void onClicked(View v); }
fonte
Portanto, esta resposta é fornecida pela biblioteca ButterKnife.
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); }
Este método trata os cliques somente após o clique anterior ter sido tratado e observe que evita vários cliques em um quadro.
fonte
Um
Handler
acelerador baseado no aplicativo Signal .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); } }
Exemplo de uso:
throttler.publish(() -> Log.d("TAG", "Example"));
Exemplo de uso em
OnClickListener
:view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));
Exemplo de uso de Kt:
view.setOnClickListener { throttler.publish { Log.d("TAG", "Example") } }
Ou com uma extensão:
fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) { throttler.publish(function) }
Em seguida, exemplo de uso:
view.setThrottledOnClickListener(throttler) { Log.d("TAG", "Example") }
fonte
Eu uso essa classe junto com a ligação de dados. Funciona bem.
/** * 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)) } } } }
E meu layout se parece com este
<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}" />
fonte
A maneira mais significativa de lidar com esse cenário é usando o operador Throttling (Throttle First) com RxJava2. Etapas para conseguir isso em Kotlin:
1). Dependências: - Adicione a dependência rxjava2 no arquivo de nível de aplicativo build.gradle.
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.10'
2). Construa uma classe abstrata que implemente View.OnClickListener e contenha o primeiro operador de aceleração para manipular o método OnClick () da visualização. O snippet de código é como:
import android.util.Log import android.view.View import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.subjects.PublishSubject import java.util.concurrent.TimeUnit abstract class SingleClickListener : View.OnClickListener { private val publishSubject: PublishSubject<View> = PublishSubject.create() private val THRESHOLD_MILLIS: Long = 600L abstract fun onClicked(v: View) override fun onClick(p0: View?) { if (p0 != null) { Log.d("Tag", "Clicked occurred") publishSubject.onNext(p0) } } init { publishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { v -> onClicked(v) } } }
3). Implemente esta classe SingleClickListener no clique da visualização em atividade. Isso pode ser alcançado como:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val singleClickListener = object : SingleClickListener(){ override fun onClicked(v: View) { // operation on click of xm_view_id } } xm_viewl_id.setOnClickListener(singleClickListener) }
Implementar essas etapas acima no aplicativo pode simplesmente evitar os vários cliques em uma visualização de até 600 ms. Boa codificação!
fonte
Você pode usar Rxbinding3 para essa finalidade. Basta adicionar esta dependência em build.gradle
build.gradle
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
Então, em sua atividade ou fragmento, use o código abaixo
your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe { // your action }
fonte
Isso é resolvido assim
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()); } });
fonte
Aqui está uma solução bastante simples, que pode ser usada com lambdas:
view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));
Aqui está o snippet pronto para copiar / colar:
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); } }
Aproveitar!
fonte
Com base na resposta de @GreyBeardedGeek ,
debounceClick_last_Timestamp
emids.xml
para marcar o carimbo de data / hora do clique anterior.Adicione este bloco de código em
BaseActivity
protected void debounceClick(View clickedView, DebouncedClick callback){ debounceClick(clickedView,1000,callback); } protected void debounceClick(View clickedView,long minimumInterval, DebouncedClick callback){ Long previousClickTimestamp = (Long) clickedView.getTag(R.id.debounceClick_last_Timestamp); long currentTimestamp = SystemClock.uptimeMillis(); clickedView.setTag(R.id.debounceClick_last_Timestamp, currentTimestamp); if(previousClickTimestamp == null || Math.abs(currentTimestamp - previousClickTimestamp) > minimumInterval) { callback.onClick(clickedView); } } public interface DebouncedClick{ void onClick(View view); }
Uso:
view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { debounceClick(v, 3000, new DebouncedClick() { @Override public void onClick(View view) { doStuff(view); // Put your's click logic on doStuff function } }); } });
Usando lambda
view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
fonte
Coloque um pequeno exemplo aqui
view.safeClick { doSomething() }
@SuppressLint("CheckResult") fun View.safeClick(invoke: () -> Unit) { RxView .clicks(this) .throttleFirst(300, TimeUnit.MILLISECONDS) .subscribe { invoke() } }
fonte
Minha solução, preciso chamar
removeall
quando sairmos (destruirmos) do fragmento e da atividade: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) } }
fonte
Eu diria que a maneira mais fácil é usar uma biblioteca de "carregamento" como o KProgressHUD.
https://github.com/Kaopiz/KProgressHUD
A primeira coisa no método onClick seria chamar a animação de carregamento que bloqueia instantaneamente toda a IU até que o desenvolvedor decida liberá-la.
Então você teria isso para a ação onClick (ela usa Butterknife, mas obviamente funciona com qualquer tipo de abordagem):
Além disso, não se esqueça de desativar o botão após o clique.
@OnClick(R.id.button) void didClickOnButton() { startHUDSpinner(); button.setEnabled(false); doAction(); }
Então:
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; }
E você pode usar o método stopHUDSpinner no método doAction () se desejar:
private void doAction(){ // some action stopHUDSpinner() }
Reative o botão de acordo com a lógica do seu aplicativo: button.setEnabled (true);
fonte