Evite los botones múltiples clics rápidos

86

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

Serpiente
fuente
¿Podemos haber implementado código utilizado código GreyBeardedGeek? ¿No estoy seguro de cómo usó la clase abstracta en su actividad?
Código
Pruebe esta solución también stackoverflow.com/questions/20971484/…
DmitryBoyko
Resolví
arekolek

Respuestas:

111

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 onDebouncedClicklugar 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);
        }
    }
}
GreyBeardedGeek
fuente
3
¡Oh, si solo todos los sitios web se molestaran en implementar algo en este sentido! ¡No más "no haga clic en enviar dos veces o se le cobrará dos veces"! Gracias.
Floris
2
Hasta donde yo sé, no. El onClick siempre debe ocurrir en el hilo de la interfaz de usuario (solo hay uno). Por lo tanto, los clics múltiples, aunque cercanos en el tiempo, siempre deben ser secuenciales, en el mismo hilo.
GreyBeardedGeek
45
@FengDai Interesante: este "desorden" hace lo que hace su biblioteca, en aproximadamente 1/20 de la cantidad de código. Tiene una solución alternativa interesante, pero este no es el lugar para insultar el código de trabajo.
GreyBeardedGeek
13
@GreyBeardedGeek Perdón por usar esa mala palabra.
Feng Dai
2
Esto es estrangulamiento, no supresión de rebotes.
Rewieer
71

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.gradlepara agregar la dependencia RxBinding:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
fuente
2
La mejor decisión. Solo tenga en cuenta que esto tiene un vínculo fuerte a su vista, por lo que el fragmento no se recopilará después de que finalice el ciclo de vida habitual; envuélvalo en la biblioteca de ciclo de vida de Trello. Luego, se cancelará la suscripción y se liberará la vista después de que se llame al método de ciclo de vida elegido (generalmente enDestroyView)
Den Drobiazko
2
Esto no funciona con adaptadores, aún puede hacer clic en varios elementos y hará el acelerador Primero, pero en cada vista individual.
desgraci
1
Se puede implementar con Handler fácilmente, no es necesario usar RxJava aquí.
Miha_x64
20

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);
            }
        };
    }
}
BoD
fuente
¿Cómo usar el método de envoltura? ¿Puede dar un ejemplo? Gracias.
Mehul Joisar
1
@MehulJoisar Así:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD
10

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
}
SANAT
fuente
¡Esto es genial! Aunque creo que el término más apropiado aquí es "acelerador", no "antirrebote". es decir, clickWithThrottle ()
hopia
9

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.

Feng Dai
fuente
@Feng Dai, cómo usar esta biblioteca paraListView
CAMOBAP
¿Cómo usar esto en Android Studio?
VVB
Esta biblioteca ya no se recomienda. En su lugar, utilice RxBinding. Vea la respuesta de Nikita.
Feng Dai
7

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);
}
Christopher Perry
fuente
1
muestra y agradable!
Chulo
5

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.

NixSam
fuente
5

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.

vlazzle
fuente
4

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);
}
GaneshP
fuente
4

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.

Piyush Jain
fuente
3

Un Handleracelerador 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")
}
Weston
fuente
1

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}" />
Raul
fuente
1

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!

Abhijeet
fuente
1

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
}
Aminul Haque Aome
fuente
0

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

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!

Leo Droidcoder
fuente
0

Basado en la respuesta de @GreyBeardedGeek ,

  • Crear debounceClick_last_Timestampen ids.xmletiquetar 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));
    
Khaled Lela
fuente
0

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() }
}
Shwarz Andrei
fuente
1
proporcione un enlace o una descripción de cuál es su RxView
BekaBot
0

Mi solución, necesito llamar removeallcuando 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)
        }
    }
usuario3748333
fuente
0

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

Alex Capitaneanu
fuente