Évitez le bouton plusieurs clics rapides

86

J'ai un problème avec mon application: si l'utilisateur clique plusieurs fois rapidement sur le bouton, plusieurs événements sont générés avant même que ma boîte de dialogue en maintenant le bouton disparaisse

Je connais une solution en définissant une variable booléenne comme indicateur lorsqu'un bouton est cliqué afin que les clics futurs puissent être évités jusqu'à ce que la boîte de dialogue soit fermée. Cependant, j'ai de nombreux boutons et devoir le faire à chaque fois pour chaque bouton semble être exagéré. N'y a-t-il pas d'autre moyen dans Android (ou peut-être une solution plus intelligente) de n'autoriser que l'action d'événement générée par clic de bouton?

Ce qui est encore pire, c'est que plusieurs clics rapides semblent générer plusieurs actions d'événement avant même que la première action ne soit gérée, donc si je veux désactiver le bouton dans la méthode de gestion du premier clic, il existe déjà des actions d'événements dans la file d'attente en attente d'être traitées!

S'il vous plaît aider Merci

Serpent
la source
pouvons-nous avoir implémenté du code utilisé le code GreyBeardedGeek. Je ne sais pas comment vous avez utilisé la classe abstraite dans votre activité?
CoDe
Essayez également cette solution stackoverflow.com/questions/20971484/…
DmitryBoyko
J'ai résolu un problème similaire en utilisant la contre-pression dans RxJava
arekolek

Réponses:

111

Voici un auditeur onClick «déboncé» que j'ai écrit récemment. Vous lui indiquez quel est le nombre minimum acceptable de millisecondes entre les clics. Implémentez votre logique au onDebouncedClicklieu 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
la source
3
Oh, si seulement chaque site Web pouvait être dérangé par la mise en œuvre de quelque chose de ce genre! Fini le "ne cliquez pas sur Soumettre deux fois ou vous serez facturé deux fois"! Merci.
Floris
2
Autant que je sache, non. L'onClick doit toujours se produire sur le thread de l'interface utilisateur (il n'y en a qu'un). Ainsi, plusieurs clics, bien que proches dans le temps, doivent toujours être séquentiels, sur le même fil.
GreyBeardedGeek
45
@FengDai Intéressant - ce "désordre" fait ce que fait votre bibliothèque, dans environ 1 / 20ème de la quantité de code. Vous avez une solution alternative intéressante, mais ce n'est pas le lieu d'insulter le code de travail.
GreyBeardedGeek
13
@GreyBeardedGeek Désolé d'avoir utilisé ce mauvais mot.
Feng Dai
2
Il s'agit d'étranglement, pas de anti-rebond.
Rewieer
71

Avec RxBinding, cela peut être fait facilement. Voici un exemple:

RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> {
            // action on click
        });

Ajoutez la ligne suivante build.gradlepour ajouter la dépendance RxBinding:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
la source
2
Meilleure décision. Notez simplement que cela contient un lien fort vers votre vue, donc le fragment ne sera pas collecté une fois le cycle de vie habituel terminé - enveloppez-le dans la bibliothèque Lifecycle de Trello. Ensuite, il sera désabonné et la vue sera libérée après l'appel de la méthode de cycle de vie choisie (généralement onDestroyView)
Den Drobiazko
2
Cela ne fonctionne pas avec les adaptateurs, peut toujours cliquer sur plusieurs éléments, et cela fera la manette des gaz d'abord mais sur chaque vue individuelle.
desgraci
1
Peut être implémenté facilement avec Handler, pas besoin d'utiliser RxJava ici.
Miha_x64
20

Voici ma version de la réponse acceptée. C'est très similaire, mais n'essaye pas de stocker des vues dans une carte, ce qui ne me semble pas une si bonne idée. Il ajoute également une méthode d'enroulement qui pourrait être utile dans de nombreuses situations.

/**
 * 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
la source
Comment utiliser la méthode wrap? Pouvez-vous donner un exemple, merci.
Mehul Joisar
1
@MehulJoisar Comme ça:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD
10

Nous pouvons le faire sans aucune bibliothèque. Créez simplement une seule fonction d'extension:

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

Voir onClick en utilisant le code ci-dessous:

buttonShare.clickWithDebounce { 
   // Do anything you want
}
SANAT
la source
C'est cool! Bien que je pense que le terme le plus approprié ici est "accélérateur" et non "anti-rebond". ie clickWithThrottle ()
hopia
9

Vous pouvez utiliser ce projet: https://github.com/fengdai/clickguard pour résoudre ce problème avec une seule instruction:

ClickGuard.guard(button);

MISE À JOUR: Cette bibliothèque n'est plus recommandée. Je préfère la solution de Nikita. Utilisez plutôt RxBinding.

Feng Dai
la source
@Feng Dai, comment utiliser cette bibliothèque pourListView
CAMOBAP
Comment l'utiliser dans Android Studio?
VVB
Cette bibliothèque n'est plus recommandée. Veuillez utiliser RxBinding à la place. Voir la réponse de Nikita.
Feng Dai
7

Voici un exemple simple:

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
la source
1
échantillon et sympa!
Chulo
5

Juste une mise à jour rapide sur la solution GreyBeardedGeek. Modifiez la clause if et ajoutez la fonction Math.abs. Réglez-le comme ceci:

  if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) {
        onDebouncedClick(clickedView);
    }

L'utilisateur peut changer l'heure sur un appareil Android et la mettre dans le passé, donc sans cela, cela pourrait entraîner un bug.

PS: je n'ai pas assez de points pour commenter votre solution, alors je viens de mettre une autre réponse.

NixSam
la source
5

Voici quelque chose qui fonctionnera avec n'importe quel événement, pas seulement des clics. Il fournira également le dernier événement même s'il fait partie d'une série d'événements rapides (comme 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) Construisez Debouncer dans un champ de l'auditeur mais en dehors de la fonction de rappel, configuré avec timeout et le callback fn que vous voulez limiter.

2) Appelez la méthode de spam de votre Debouncer dans la fonction de rappel de l'auditeur.

vlazzle
la source
4

Solution similaire utilisant 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
la source
4

Donc, cette réponse est fournie par la bibliothèque 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);
}

Cette méthode gère les clics uniquement après le clic précédent et notez qu'elle évite plusieurs clics dans un cadre.

Piyush Jain
la source
3

Un Handlerthrottler basé sur Signal App .

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

Exemple d'utilisation:

throttler.publish(() -> Log.d("TAG", "Example"));

Exemple d'utilisation dans un OnClickListener:

view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));

Exemple d'utilisation de Kt:

view.setOnClickListener {
    throttler.publish {
        Log.d("TAG", "Example")
    }
}

Ou avec une extension:

fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) {
    throttler.publish(function)
}

Puis exemple d'utilisation:

view.setThrottledOnClickListener(throttler) {
    Log.d("TAG", "Example")
}
Weston
la source
1

J'utilise cette classe avec la liaison de données. Fonctionne très bien.

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

Et ma mise en page ressemble à ceci

<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
la source
1

Un moyen plus significatif de gérer ce scénario consiste à utiliser l'opérateur de limitation (Throttle First) avec RxJava2. Étapes pour y parvenir à Kotlin:

1). Dépendances: - Ajoutez une dépendance rxjava2 dans le fichier de niveau application build.gradle.

implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.10'

2). Construisez une classe abstraite qui implémente View.OnClickListener et contient le premier opérateur de limitation pour gérer la méthode OnClick () de la vue. L'extrait de code est comme suit:

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). Implémentez cette classe SingleClickListener sur le clic de vue dans l'activité. Ceci peut être réalisé comme:

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 mise en œuvre de ces étapes ci-dessus dans l'application peut simplement éviter les multiples clics sur une vue jusqu'à 600 ms. Bon codage!

Abhijeet
la source
1

Vous pouvez utiliser Rxbinding3 à cette fin. Ajoutez simplement cette dépendance dans build.gradle

build.gradle

implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'

Puis dans votre activité ou fragment, utilisez le code ci-dessous

your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe {
    // your action
}
Aminul Haque Aome
la source
0

C'est résolu comme ça

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
la source
0

Voici une solution assez simple, qui peut être utilisée avec des lambdas:

view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));

Voici l'extrait de code prêt à copier / coller:

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

Prendre plaisir!

Leo Droidcoder
la source
0

Basé sur la réponse de @GreyBeardedGeek ,

  • Créer debounceClick_last_Timestampsur ids.xmlpour marquer l'horodatage du clic précédent.
  • Ajoutez ce bloc de code dans 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);
    }
    
  • Usage:

    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
                }
            });
        }
    });
    
  • Utilisation de lambda

    view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
    
Khaled Lela
la source
0

Mettez un petit exemple ici

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}
Shwarz Andrei
la source
1
Veuillez fournir un lien ou une description de votre RxView
BekaBot
0

Ma solution, besoin d'appeler removealllorsque nous quittons (détruisons) le fragment et l'activité:

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)
        }
    }
utilisateur3748333
la source
0

Je dirais que le moyen le plus simple est d'utiliser une bibliothèque de "chargement" comme KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

La première chose à faire avec la méthode onClick serait d'appeler l'animation de chargement qui bloque instantanément toute l'interface utilisateur jusqu'à ce que le développeur décide de la libérer.

Donc, vous auriez ceci pour l'action onClick (cela utilise Butterknife mais cela fonctionne évidemment avec n'importe quel type d'approche):

N'oubliez pas non plus de désactiver le bouton après le clic.

@OnClick(R.id.button)
void didClickOnButton() {
    startHUDSpinner();
    button.setEnabled(false);
    doAction();
}

Ensuite:

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

Et vous pouvez utiliser la méthode stopHUDSpinner dans la méthode doAction () si vous le souhaitez:

private void doAction(){ 
   // some action
   stopHUDSpinner()
}

Réactivez le bouton en fonction de la logique de votre application: button.setEnabled (true);

Alex Capitaneanu
la source