Vermeiden Sie mehrere schnelle Klicks

84

Ich habe ein Problem mit meiner App: Wenn der Benutzer mehrmals schnell auf die Schaltfläche klickt, werden mehrere Ereignisse generiert, bevor selbst mein Dialogfeld mit der Schaltfläche verschwindet

Ich kenne eine Lösung, indem ich eine boolesche Variable als Flag setze, wenn auf eine Schaltfläche geklickt wird, damit zukünftige Klicks verhindert werden können, bis der Dialog geschlossen wird. Ich habe jedoch viele Knöpfe und dies jedes Mal für jeden Knopf tun zu müssen, scheint ein Overkill zu sein. Gibt es in Android keine andere Möglichkeit (oder vielleicht eine intelligentere Lösung), nur Ereignisaktionen zuzulassen, die pro Schaltflächenklick generiert werden?

Was noch schlimmer ist, ist, dass mehrere schnelle Klicks mehrere Ereignisaktionen zu generieren scheinen, bevor die erste Aktion ausgeführt wird. Wenn ich also die Schaltfläche in der ersten Klickbehandlungsmethode deaktivieren möchte, sind bereits Ereignisaktionen in der Warteschlange vorhanden, die darauf warten, behandelt zu werden!

Bitte helfen Sie Danke

Schlange
quelle
Können wir Code verwendet haben, der GreyBeardedGeek-Code verwendet? Ich bin mir nicht sicher, wie Sie die abstrakte Klasse in Ihrer Aktivität verwendet haben?
CoDe
Versuchen Sie auch diese Lösung stackoverflow.com/questions/20971484/…
Entwickler
Ich habe ein ähnliches Problem mit Gegendruck in RxJava gelöst
Arekolek

Antworten:

110

Hier ist ein "entprellter" onClick-Listener, den ich kürzlich geschrieben habe. Sie geben an, wie viele Millisekunden zwischen den Klicks mindestens zulässig sind. Implementieren Sie Ihre Logik in onDebouncedClickstattonClick

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
quelle
3
Oh, wenn nur jede Website die Mühe hätte, etwas in diese Richtung zu implementieren! Nicht mehr "Klicken Sie nicht zweimal auf Senden, sonst werden Sie zweimal belastet"! Danke dir.
Floris
2
Soweit ich weiß, nein. Der onClick sollte immer im UI-Thread erfolgen (es gibt nur einen). Daher sollten mehrere Klicks, obwohl sie zeitlich eng sind, immer aufeinanderfolgend auf demselben Thread erfolgen.
GreyBeardedGeek
44
@FengDai Interessant - dieses "Durcheinander" macht das, was Ihre Bibliothek macht, in etwa 1/20 der Codemenge. Sie haben eine interessante alternative Lösung, aber dies ist nicht der richtige Ort, um Arbeitscode zu beleidigen.
GreyBeardedGeek
13
@GreyBeardedGeek Tut mir leid, dass ich dieses schlechte Wort verwendet habe.
Feng Dai
2
Dies ist Drosselung, nicht Entprellen.
Rewieer
70

Mit RxBinding ist dies problemlos möglich. Hier ist ein Beispiel:

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

Fügen Sie die folgende Zeile hinzu build.gradle, um die RxBinding-Abhängigkeit hinzuzufügen:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
quelle
2
Beste Entscheidung. Beachten Sie nur, dass dies eine starke Verbindung zu Ihrer Ansicht darstellt, sodass Fragmente nach Abschluss des normalen Lebenszyklus nicht erfasst werden. Wickeln Sie es in die Trello-Lebenszyklusbibliothek ein. Dann wird es abgemeldet und die Ansicht freigegeben, nachdem die gewählte Lebenszyklusmethode aufgerufen wurde (normalerweise onDestroyView)
Den Drobiazko
2
Dies funktioniert nicht mit Adaptern, kann jedoch immer noch auf mehrere Elemente klicken, und es wird das ThrottleFirst ausgeführt, jedoch für jede einzelne Ansicht.
Desgraci
1
Kann mit Handler einfach implementiert werden, hier muss RxJava nicht verwendet werden.
Miha_x64
20

Hier ist meine Version der akzeptierten Antwort. Es ist sehr ähnlich, versucht aber nicht, Ansichten in einer Karte zu speichern, was ich nicht für eine so gute Idee halte. Außerdem wird eine Wrap-Methode hinzugefügt, die in vielen Situationen nützlich sein kann.

/**
 * 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
quelle
Wie verwende ich die Wrap-Methode? Können Sie bitte ein Beispiel geben. Danke.
Mehul Joisar
1
@MehulJoisar Gefällt mir:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD
10

Wir können es ohne Bibliothek tun. Erstellen Sie einfach eine einzige Erweiterungsfunktion:

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

Zeigen Sie onClick mit dem folgenden Code an:

buttonShare.clickWithDebounce { 
   // Do anything you want
}
SANAT
quelle
Das ist cool! Obwohl ich denke, dass der passendere Begriff hier "Gas" ist, nicht "entprellen". dh clickWithThrottle ()
hopia
9

Sie können dieses Projekt verwenden: https://github.com/fengdai/clickguard , um dieses Problem mit einer einzigen Anweisung zu beheben:

ClickGuard.guard(button);

UPDATE: Diese Bibliothek wird nicht mehr empfohlen. Ich bevorzuge Nikitas Lösung. Verwenden Sie stattdessen RxBinding.

Feng Dai
quelle
@ Feng Dai, wie man diese Bibliothek fürListView
CAMOBAP
Wie benutzt man das in Android Studio?
VVB
Diese Bibliothek wird nicht mehr empfohlen. Bitte verwenden Sie stattdessen RxBinding. Siehe Nikitas Antwort.
Feng Dai
6

Hier ist ein einfaches Beispiel:

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
quelle
5

Nur ein kurzes Update der GreyBeardedGeek-Lösung. Ändern Sie die if-Klausel und fügen Sie die Math.abs-Funktion hinzu. Stellen Sie es so ein:

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

Der Benutzer kann die Uhrzeit auf einem Android-Gerät ändern und in die Vergangenheit verschieben, ohne dass dies zu Fehlern führen kann.

PS: Ich habe nicht genug Punkte, um Ihre Lösung zu kommentieren, also habe ich einfach eine andere Antwort gegeben.

NixSam
quelle
5

Hier ist etwas, das mit jedem Ereignis funktioniert, nicht nur mit Klicks. Es wird auch das letzte Ereignis liefern, selbst wenn es Teil einer Reihe von schnellen Ereignissen ist (wie z . B. 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) Erstellen Sie den Debouncer in einem Feld des Listeners, jedoch außerhalb der Rückruffunktion, konfiguriert mit Timeout und dem Rückruf fn, den Sie drosseln möchten.

2) Rufen Sie die Spam-Methode Ihres Debouncers in der Rückruffunktion des Listeners auf.

vlazzle
quelle
4

Ähnliche Lösung mit 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
quelle
4

Diese Antwort wird also von der ButterKnife-Bibliothek bereitgestellt.

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

Diese Methode verarbeitet Klicks erst, nachdem der vorherige Klick ausgeführt wurde. Beachten Sie, dass mehrere Klicks in einem Frame vermieden werden.

Piyush Jain
quelle
3

Ein Handlerbasierter Throttler von 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);
    }
}

Anwendungsbeispiel:

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

Beispielverwendung in einem OnClickListener:

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

Beispiel Kt-Verwendung:

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

Oder mit einer Erweiterung:

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

Dann Beispielverwendung:

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

Ich benutze diese Klasse zusammen mit der Datenbindung. Funktioniert super.

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

Und mein Layout sieht so aus

<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
quelle
1

Eine wichtigere Methode zur Behandlung dieses Szenarios ist die Verwendung des Throttling-Operators (Throttle First) mit RxJava2. Schritte, um dies in Kotlin zu erreichen:

1). Abhängigkeiten: - Fügen Sie die rxjava2-Abhängigkeit in der Datei auf der Ebene der build.gradle-App-Ebene hinzu.

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

2). Erstellen Sie eine abstrakte Klasse, die View.OnClickListener implementiert und den ersten Throttle-Operator enthält, um die OnClick () -Methode der Ansicht zu verarbeiten. Code-Snippet ist wie folgt:

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). Implementieren Sie diese SingleClickListener-Klasse beim Klicken auf Ansicht in Aktivität. Dies kann erreicht werden als:

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

Durch die Implementierung dieser obigen Schritte in die App können Sie einfach die mehrfachen Klicks auf eine Ansicht bis zu 600 ms vermeiden. Viel Spaß beim Codieren!

Abhijeet
quelle
0

Dies wird so gelöst

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
quelle
0

Hier ist eine ziemlich einfache Lösung, die mit Lambdas verwendet werden kann:

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

Hier ist das Copy / Paste-Ready-Snippet:

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

Genießen!

Leo Droidcoder
quelle
0

Basierend auf der Antwort von @GreyBeardedGeek ,

  • Erstellen Sie einen, debounceClick_last_Timestampum ids.xmlden vorherigen Klick-Zeitstempel zu markieren.
  • Fügen Sie diesen Codeblock hinzu 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);
    }
  • Verwendung:

    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
                }
            });
        }
    });
  • Mit Lambda

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

Geben Sie hier ein kleines Beispiel

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}
Shwarz Andrei
quelle
1
Bitte geben Sie einen Link oder eine Beschreibung an, was Ihr RxView ist
BekaBot
0

Meine Lösung muss aufgerufen werden, removeallwenn wir das Fragment und die Aktivität verlassen (zerstören):

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)
        }
    }
user3748333
quelle
0

Ich würde sagen, der einfachste Weg ist die Verwendung einer "Lade" -Bibliothek wie KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

Das erste, was bei der onClick-Methode getan wird, ist, die Ladeanimation aufzurufen, die sofort die gesamte Benutzeroberfläche blockiert, bis der Entwickler beschließt, sie freizugeben.

Sie hätten dies also für die onClick-Aktion (dies verwendet Butterknife, funktioniert aber offensichtlich mit jeder Art von Ansatz):

Vergessen Sie auch nicht, die Schaltfläche nach dem Klicken zu deaktivieren.

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

Dann:

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

Und wenn Sie möchten, können Sie die stopHUDSpinner-Methode in der doAction () -Methode verwenden:

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

Aktivieren Sie die Schaltfläche gemäß Ihrer App-Logik erneut: button.setEnabled (true);

Alex Capitaneanu
quelle