Unikaj wielokrotnych szybkich kliknięć przycisku

86

Mam problem z moją aplikacją, że jeśli użytkownik szybko kliknie przycisk kilka razy, to generowanych jest wiele zdarzeń, zanim nawet moje okno dialogowe z przyciskiem zniknie

Znam rozwiązanie, ustawiając zmienną boolowską jako flagę po kliknięciu przycisku, dzięki czemu można zapobiec przyszłym kliknięciom, dopóki okno dialogowe nie zostanie zamknięte. Jednak mam wiele przycisków i konieczność robienia tego za każdym razem dla każdego przycisku wydaje się przesadą. Czy w Androidzie nie ma innego sposobu (a może jakiegoś inteligentniejszego rozwiązania), aby zezwolić tylko na akcję zdarzenia generowaną na kliknięcie przycisku?

Co gorsza, wiele szybkich kliknięć wydaje się generować wiele akcji zdarzenia, zanim nawet pierwsza akcja zostanie obsłużona, więc jeśli chcę wyłączyć przycisk w metodzie obsługi pierwszego kliknięcia, w kolejce są już istniejące akcje zdarzeń, które czekają na obsługę!

Proszę o pomoc Dzięki

Wąż
źródło
czy możemy zaimplementować kod użyty w kodzie GreyBeardedGeek. Nie jestem pewien, w jaki sposób wykorzystałeś klasę abstrakcyjną w swojej aktywności?
CoDe
Wypróbuj również to rozwiązanie stackoverflow.com/questions/20971484/…
DmitryBoyko
Podobny problem rozwiązałem za pomocą przeciwciśnienia w RxJava
arekolek

Odpowiedzi:

111

Oto 'zdemontowany' słuchacz onClick, który niedawno napisałem. Mówisz mu, jaka jest minimalna dopuszczalna liczba milisekund między kliknięciami. Zaimplementuj swoją logikę onDebouncedClickzamiastonClick

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
źródło
3
Och, gdyby tylko każda witryna mogła mieć kłopoty z wdrażaniem czegoś w tym zakresie! Nigdy więcej „nie klikaj przycisku prześlij dwa razy, bo zostaniesz obciążony dwukrotnie”! Dziękuję Ci.
Floris
2
O ile wiem, nie. OnClick powinien zawsze mieć miejsce w wątku interfejsu użytkownika (jest tylko jeden). Tak więc wielokrotne kliknięcia zbliżone w czasie powinny zawsze następować po sobie, w tym samym wątku.
GreyBeardedGeek
45
@FengDai Interesujące - ten "bałagan" robi to, co robi twoja biblioteka, w około 1/20 części kodu. Masz ciekawe alternatywne rozwiązanie, ale to nie jest miejsce na obrażanie działającego kodu.
GreyBeardedGeek
13
@GreyBeardedGeek Przepraszamy za używanie tego złego słowa.
Feng Dai
2
To dławienie, a nie odbijanie.
Rewieer,
71

Dzięki RxBinding można to łatwo zrobić. Oto przykład:

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

Dodaj następujący wiersz, build.gradleaby dodać zależność RxBinding:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
źródło
2
Najlepsza decyzja. Zwróć uwagę, że jest to silny link do twojego widoku, więc fragment nie zostanie zebrany po zakończeniu zwykłego cyklu życia - zapakuj go do biblioteki cyklu życia Trello. Następnie zostanie anulowana subskrypcja i widok zwolniony po wywołaniu wybranej metody cyklu życia (zwykle onDestroyView)
Den Drobiazko
2
To nie działa z adapterami, nadal można kliknąć wiele elementów i spowoduje to throttleFirst, ale dla każdego pojedynczego widoku.
desgraci
1
Można go łatwo zaimplementować za pomocą programu Handler, nie ma potrzeby używania tutaj RxJava.
Miha_x64
20

Oto moja wersja zaakceptowanej odpowiedzi. Jest bardzo podobny, ale nie próbuje przechowywać widoków na mapie, co moim zdaniem nie jest dobrym pomysłem. Dodaje również metodę zawijania, która może być przydatna w wielu sytuacjach.

/**
 * 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
źródło
Jak stosować metodę zawijania? Czy możesz podać przykład. Dzięki.
Mehul Joisar
1
@MehulJoisar W ten sposób:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD
10

Możemy to zrobić bez biblioteki. Po prostu utwórz jedną funkcję rozszerzenia:

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

Zobacz onClick używając poniższego kodu:

buttonShare.clickWithDebounce { 
   // Do anything you want
}
SANAT
źródło
To jest fajne! Chociaż wydaje mi się, że bardziej odpowiednim terminem jest tutaj „dławienie”, a nie „osłabienie”. tj. clickWithThrottle ()
hopia
9

Możesz użyć tego projektu: https://github.com/fengdai/clickguard, aby rozwiązać ten problem za pomocą jednej instrukcji:

ClickGuard.guard(button);

UPDATE: ta biblioteka nie jest już zalecana. Wolę rozwiązanie Nikity. Zamiast tego użyj RxBinding.

Feng Dai
źródło
@Feng Dai, jak korzystać z tej bibliotekiListView
CAMOBAP
Jak tego używać w Android Studio?
VVB
Ta biblioteka nie jest już zalecana. Zamiast tego użyj RxBinding. Zobacz odpowiedź Nikity.
Feng Dai,
7

Oto prosty przykład:

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
źródło
1
próbka i miło!
Chulo
5

Tylko szybka aktualizacja rozwiązania GreyBeardedGeek. Zmień klauzulę if i dodaj funkcję Math.abs. Ustaw to tak:

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

Użytkownik może zmienić czas na urządzeniu z Androidem i umieścić go w przeszłości, więc bez tego może to doprowadzić do błędu.

PS: nie mam wystarczająco dużo punktów, aby skomentować twoje rozwiązanie, więc po prostu podaję inną odpowiedź.

NixSam
źródło
5

Oto coś, co będzie działać z każdym wydarzeniem, nie tylko z kliknięciami. Dostarcza również ostatnie zdarzenie, nawet jeśli jest częścią serii szybkich zdarzeń (takich jak odbicie rx ).

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) Skonstruuj Debouncer w polu nasłuchiwania, ale poza funkcją wywołania zwrotnego, skonfigurowanym z limitem czasu i fn wywołania zwrotnego, które chcesz ograniczyć.

2) Wywołaj metodę spamu swojego Debouncera w funkcji wywołania zwrotnego słuchacza.

vlazzle
źródło
4

Podobne rozwiązanie przy użyciu 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
źródło
4

Tak więc ta odpowiedź jest dostarczana przez bibliotekę 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);
}

Ta metoda obsługuje kliknięcia dopiero po obsłużeniu poprzedniego kliknięcia i pamiętaj, że pozwala uniknąć wielu kliknięć w ramce.

Piyush Jain
źródło
3

HandlerOparty throttler z App Sygnału .

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

Przykładowe użycie:

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

Przykładowe użycie w OnClickListener:

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

Przykładowe użycie Kt:

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

Lub z rozszerzeniem:

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

Następnie przykładowe użycie:

view.setThrottledOnClickListener(throttler) {
    Log.d("TAG", "Example")
}
Weston
źródło
1

Używam tej klasy razem z wiązaniem danych. Działa świetnie.

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

A mój układ wygląda tak

<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
źródło
1

Bardziej znaczącym sposobem obsługi tego scenariusza jest użycie operatora Throttling (Throttle First) z RxJava2. Kroki do osiągnięcia tego w Kotlinie:

1). Zależności: - Dodaj zależność rxjava2 w pliku poziomu aplikacji build.gradle.

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

2). Skonstruuj klasę abstrakcyjną, która implementuje View.OnClickListener i zawiera pierwszy operator ograniczający do obsługi metody OnClick () widoku. Fragment kodu jest taki:

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). Zaimplementuj tę klasę SingleClickListener po kliknięciu widoku w działaniu. Można to osiągnąć jako:

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

Wykonanie powyższych czynności w aplikacji może po prostu uniknąć wielokrotnych kliknięć widoku do 600 ms. Miłego kodowania!

Abhijeet
źródło
1

W tym celu możesz użyć Rxbinding3. Po prostu dodaj tę zależność w pliku build.gradle

build.gradle

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

Następnie w swojej aktywności lub fragmencie użyj poniższego kodu

your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe {
    // your action
}
Aminul Haque Aome
źródło
0

To jest rozwiązane w ten sposób

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
źródło
0

Oto całkiem proste rozwiązanie, którego można używać z lambdami:

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

Oto gotowy fragment kodu do kopiowania / wklejania:

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

Cieszyć się!

Leo Droidcoder
źródło
0

Na podstawie odpowiedzi @GreyBeardedGeek ,

  • Utwórz debounceClick_last_Timestampw ids.xmlcelu otagowania sygnatury czasowej poprzedniego kliknięcia.
  • Dodaj ten blok kodu do 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);
    }
    
  • Stosowanie:

    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
                }
            });
        }
    });
    
  • Korzystanie z lambda

    view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
    
Khaled Lela
źródło
0

Podaj tutaj mały przykład

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}
Shwarz Andrei
źródło
1
proszę podać link lub opis, jaki jest twój RxView
BekaBot
0

Moje rozwiązanie, trzeba zadzwonić, removeallgdy wyjdziemy (zniszczymy) z fragmentu i aktywności:

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
źródło
0

Powiedziałbym, że najłatwiejszym sposobem jest użycie biblioteki „ładującej”, takiej jak KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

Pierwszą rzeczą w metodzie onClick byłoby wywołanie animacji ładowania, która natychmiast blokuje cały interfejs użytkownika, dopóki programista nie zdecyduje się go zwolnić.

Więc miałbyś to dla akcji onClick (to używa Butterknife, ale oczywiście działa z każdym podejściem):

Nie zapomnij też wyłączyć przycisku po kliknięciu.

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

Następnie:

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

Jeśli chcesz, możesz użyć metody stopHUDSpinner w metodzie doAction ():

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

Ponownie włącz przycisk zgodnie z logiką aplikacji: button.setEnabled (true);

Alex Capitaneanu
źródło