Tengo un problema con mi aplicación que si el usuario hace clic en el botón varias veces rápidamente, se generan varios eventos antes de que desaparezca incluso mi diálogo que mantiene presionado el botón
Conozco una solución estableciendo una variable booleana como una bandera cuando se hace clic en un botón para evitar futuros clics hasta que se cierre el cuadro de diálogo. Sin embargo, tengo muchos botones y tener que hacer esto cada vez para cada botón parece ser una exageración. ¿No hay otra forma en Android (o tal vez alguna solución más inteligente) para permitir solo la acción de evento generada por clic en el botón?
Lo que es aún peor es que múltiples clics rápidos parecen generar múltiples acciones de eventos incluso antes de que se maneje la primera acción, por lo que si quiero deshabilitar el botón en el método de manejo del primer clic, ¡ya hay acciones de eventos existentes en la cola esperando ser manejadas!
Por favor ayuda gracias
Respuestas:
Aquí hay un oyente onClick 'sin rebotes' que escribí recientemente. Usted le dice cuál es el número mínimo aceptable de milisegundos entre clics. Implemente su lógica en
onDebouncedClick
lugar 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); } } }
fuente
Con RxBinding se puede hacer fácilmente. Aquí hay un ejemplo:
RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> { // action on click });
Agregue la siguiente línea
build.gradle
para agregar la dependencia RxBinding:compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
fuente
Aquí está mi versión de la respuesta aceptada. Es muy similar, pero no intenta almacenar vistas en un mapa, lo cual no creo que sea una buena idea. También agrega un método de ajuste que podría ser útil en muchas situaciones.
/** * 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); } }; } }
fuente
myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
Podemos hacerlo sin biblioteca. Simplemente cree una sola función de extensión:
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() } }) }
Ver onClick usando el siguiente código:
buttonShare.clickWithDebounce { // Do anything you want }
fuente
Puede utilizar este proyecto: https://github.com/fengdai/clickguard para resolver este problema con una sola declaración:
ClickGuard.guard(button);
ACTUALIZAR: Esta biblioteca ya no se recomienda. Prefiero la solución de Nikita. En su lugar, utilice RxBinding.
fuente
ListView
He aquí un ejemplo sencillo:
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); }
fuente
Solo una actualización rápida sobre la solución GreyBeardedGeek. Cambie la cláusula if y agregue la función Math.abs. Configúrelo así:
if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) { onDebouncedClick(clickedView); }
El usuario puede cambiar la hora en el dispositivo Android y ponerla en pasado, por lo que sin esto podría provocar errores.
PD: no tengo suficientes puntos para comentar tu solución, así que solo pongo otra respuesta.
fuente
Aquí hay algo que funcionará con cualquier evento, no solo con clics. También entregará el último evento incluso si es parte de una serie 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) Construya Debouncer en un campo del oyente pero fuera de la función de devolución de llamada, configurado con el tiempo de espera y la fn de devolución de llamada que desea limitar.
2) Llame al método de spam de su Debouncer en la función de devolución de llamada del oyente.
fuente
Solución similar 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); }
fuente
Entonces, esta respuesta la proporciona la 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 maneja los clics solo después de que se haya manejado el clic anterior y tenga en cuenta que evita múltiples clics en un marco.
fuente
Un
Handler
acelerador basado en la aplicación 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); } }
Uso de ejemplo:
throttler.publish(() -> Log.d("TAG", "Example"));
Ejemplo de uso en un
OnClickListener
:view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));
Ejemplo de uso de Kt:
view.setOnClickListener { throttler.publish { Log.d("TAG", "Example") } }
O con una extensión:
fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) { throttler.publish(function) }
Entonces uso de ejemplo:
view.setThrottledOnClickListener(throttler) { Log.d("TAG", "Example") }
fuente
Utilizo esta clase junto con el enlace de datos. Funciona genial.
/** * 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)) } } } }
Y mi diseño se ve así
<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}" />
fuente
Una forma más significativa de manejar este escenario es usar el operador Throttling (Throttle First) con RxJava2. Pasos para lograr esto en Kotlin:
1). Dependencias: - Agregue la dependencia rxjava2 en el archivo de nivel de aplicación build.gradle.
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.10'
2). Construya una clase abstracta que implemente View.OnClickListener y contenga el primer operador del acelerador para manejar el método OnClick () de la vista. El fragmento de código es 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 clase SingleClickListener en el clic de la vista en la actividad. Esto se puede lograr 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) }
La implementación de estos pasos anteriores en la aplicación puede simplemente evitar los múltiples clics en una vista hasta 600 mS. ¡Feliz codificación!
fuente
Puede usar Rxbinding3 para ese propósito. Simplemente agregue esta dependencia en build.gradle
build.gradle
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
Luego, en su actividad o fragmento, use el siguiente código
your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe { // your action }
fuente
Esto se resuelve así
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()); } });
fuente
Aquí hay una solución bastante simple, que se puede usar con lambdas:
view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));
Aquí está el fragmento listo para copiar / pegar:
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); } }
¡Disfrutar!
fuente
Basado en la respuesta de @GreyBeardedGeek ,
debounceClick_last_Timestamp
enids.xml
etiquetar anterior Click marca de tiempo.Agregue este bloque de código a
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));
fuente
Pon un pequeño ejemplo aquí
view.safeClick { doSomething() }
@SuppressLint("CheckResult") fun View.safeClick(invoke: () -> Unit) { RxView .clicks(this) .throttleFirst(300, TimeUnit.MILLISECONDS) .subscribe { invoke() } }
fuente
Mi solución, necesito llamar
removeall
cuando salimos (destruimos) del fragmento y la actividad: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) } }
fuente
Yo diría que la forma más fácil es usar una biblioteca de "carga" como KProgressHUD.
https://github.com/Kaopiz/KProgressHUD
Lo primero en el método onClick sería llamar a la animación de carga que bloquea instantáneamente toda la interfaz de usuario hasta que el desarrollador decida liberarla.
Entonces, tendría esto para la acción onClick (esto usa Butterknife pero obviamente funciona con cualquier tipo de enfoque):
Además, no olvide desactivar el botón después del clic.
@OnClick(R.id.button) void didClickOnButton() { startHUDSpinner(); button.setEnabled(false); doAction(); }
Entonces:
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; }
Y puede usar el método stopHUDSpinner en el método doAction () si así lo desea:
private void doAction(){ // some action stopHUDSpinner() }
Vuelva a habilitar el botón de acuerdo con la lógica de su aplicación: button.setEnabled (true);
fuente