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
Réponses:
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
onDebouncedClick
lieu 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); } } }
la source
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.gradle
pour ajouter la dépendance RxBinding:compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
la source
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); } }; } }
la source
myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
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 }
la source
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.
la source
ListView
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); }
la source
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.
la source
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.
la source
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); }
la source
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.
la source
Un
Handler
throttler 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") }
la source
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}" />
la source
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!
la source
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 }
la source
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()); } });
la source
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!
la source
Basé sur la réponse de @GreyBeardedGeek ,
debounceClick_last_Timestamp
surids.xml
pour 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));
la source
Mettez un petit exemple ici
view.safeClick { doSomething() }
@SuppressLint("CheckResult") fun View.safeClick(invoke: () -> Unit) { RxView .clicks(this) .throttleFirst(300, TimeUnit.MILLISECONDS) .subscribe { invoke() } }
la source
Ma solution, besoin d'appeler
removeall
lorsque 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) } }
la source
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);
la source