Birden çok hızlı tıklamadan kaçının


88

Uygulamamla ilgili bir sorun yaşıyorum, kullanıcı düğmeyi birden çok kez hızlı bir şekilde tıklarsa, düğmeyi basılı tuttuğum iletişim kutusu kaybolmadan önce birden çok olay üretiliyor

Bir düğmeye tıklandığında bir boole değişkenini bayrak olarak ayarlayarak bir çözüm biliyorum, böylece iletişim kutusu kapanana kadar gelecekteki tıklamalar engellenebilir. Bununla birlikte, birçok düğmem var ve bunu her düğme için her seferinde yapmak zorunda kalmak bir abartı gibi görünüyor. Android'de (veya belki daha akıllı bir çözümde) yalnızca düğme tıklaması başına oluşturulan olay eylemine izin vermenin başka bir yolu yok mu?

Daha da kötüsü, birden çok hızlı tıklamanın, ilk eylem bile işlenmeden önce birden çok olay eylemi oluşturmasıdır, bu nedenle, ilk tıklama işleme yönteminde düğmeyi devre dışı bırakmak istersem, kuyrukta halihazırda işlenmeyi bekleyen mevcut olaylar eylemleri vardır!

Lütfen yardım edin, teşekkürler


GreyBeardedGeek kodunu kullanan kodu uygulayabilir miyiz? Aktivitenizde soyut sınıfı nasıl kullandığınızdan emin değilim?
CoDe


Yanıtlar:


114

İşte yakın zamanda yazdığım bir onClick dinleyicisi. Tıklamalar arasındaki kabul edilebilir minimum milisaniye sayısının ne olduğunu söylersiniz. onDebouncedClickBunun yerine mantığınızı uygulayınonClick

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

3
Keşke her web sitesi bu satırlar boyunca bir şeyler uygulamaktan rahatsız olsaydı! Artık "iki kez gönder'e tıklamayın, aksi takdirde iki kez ücretlendirileceksiniz"! Teşekkür ederim.
Floris

2
Bildiğim kadarıyla hayır. OnClick her zaman UI iş parçacığında gerçekleşmelidir (yalnızca bir tane vardır). Bu nedenle, birden fazla tıklama, zaman içinde yakın olsa da, her zaman aynı ileti dizisinde sıralı olmalıdır.
GreyBeardedGeek

45
@FengDai İlginç - bu "karışıklık", kütüphanenizin yaptığı şeyi, kod miktarının yaklaşık 1 / 20'sinde yapar. İlginç bir alternatif çözümünüz var, ancak burası çalışma kodunu aşağılamanın yeri değil.
GreyBeardedGeek

13
@GreyBeardedGeek Bu kötü kelimeyi kullandığım için üzgünüm.
Feng Dai

2
Bu, hata yapmak değil, kısmaktır.
Rewieer

71

RxBinding ile kolaylıkla yapılabilir. İşte bir örnek:

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

build.gradleRxBinding bağımlılığı eklemek için aşağıdaki satırı ekleyin:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'

2
En iyi karar. Bunun görünümünüzle güçlü bir bağlantıya sahip olduğunu ve böylece normal yaşam döngüsü tamamlandıktan sonra parça toplanmayacağını unutmayın - Trello'nun Yaşam Döngüsü kitaplığına sarın. Ardından, seçilen yaşam döngüsü yöntemi çağrıldıktan sonra abonelikten çıkarılacak ve görünüm serbest bırakılacaktır (genellikle onDestroyView)
Den Drobiazko

2
Bu adaptörler ile çalışmaz, yine de birden fazla öğeye tıklayabilir ve throttleFirst'i ancak her bir görünümde gerçekleştirir.
desgraci

1
Handler ile kolayca uygulanabilir, burada RxJava kullanmaya gerek yok.
Miha_x64

20

İşte kabul edilen cevabın versiyonum. Çok benzer, ancak Görünümleri bir Haritada saklamaya çalışmıyor ki bu çok da iyi bir fikir değil. Ayrıca, birçok durumda yararlı olabilecek bir sarma yöntemi de ekler.

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

Sarma yöntemi nasıl kullanılır? Lütfen bir örnek verir misiniz, teşekkürler.
Mehul Joisar

1
@MehulJoisar gibi bu:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
YK

11

Bunu herhangi bir kütüphane olmadan da yapabiliriz. Tek bir uzantı işlevi oluşturun:

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

Aşağıdaki kodu kullanarak onClick'i görüntüleyin:

buttonShare.clickWithDebounce { 
   // Do anything you want
}

Bu havalı! Bence burada daha uygun olan terim "geri çekilme" değil "gaz kelebeği". yani clickWithThrottle ()
hopia

private var lastClickTime: Long = 0setOnClickListener {...} dışına taşınmalı veya lastClickTime her zaman 0 olacaktır.
Robert

9

Bu sorunu tek bir ifadeyle çözmek için bu projeyi kullanabilirsiniz: https://github.com/fengdai/clickguard :

ClickGuard.guard(button);

GÜNCELLEME: Bu kitaplık artık önerilmemektedir. Nikita'nın çözümünü tercih ediyorum. Bunun yerine RxBinding'i kullanın.


@Feng Dai, bu kitaplık nasıl kullanılırListView
CAMOBAP

Bunu android stüdyosunda nasıl kullanırım?
VVB

Bu kitaplık artık tavsiye edilmiyor. Lütfen bunun yerine RxBinding'i kullanın. Nikita'nın cevabına bakın.
Feng Dai

7

İşte basit bir örnek:

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

1
örnek ve güzel!
Chulo

5

GreyBeardedGeek çözümüyle ilgili hızlı bir güncelleme. İf cümlesini değiştirin ve Math.abs işlevini ekleyin. Bunu şu şekilde ayarlayın:

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

Kullanıcı Android cihazda saati değiştirebilir ve geçmişe koyabilir, bu nedenle bu olmadan hataya yol açabilir.

Not: Çözümünüz hakkında yorum yapmak için yeterli puanınız yok, bu yüzden sadece başka bir cevap verdim.


5

İşte sadece tıklamalarla değil, herhangi bir etkinlikte işe yarayacak bir şey. Ayrıca, bir seri hızlı olayın parçası olsa bile ( rx debounce gibi ) son olayı teslim edecektir .

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) Debouncer'ı dinleyicinin bir alanında, ancak geri arama işlevinin dışında, zaman aşımı ve kısmak istediğiniz geri arama fn ile yapılandırılmış şekilde oluşturun.

2) Dinleyicinin geri arama işlevinde Debouncer'ınızın spam yöntemini çağırın.


5

Yani, bu cevap ButterKnife kütüphanesi tarafından verilmektedir.

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

Bu yöntem, tıklamaları yalnızca önceki tıklama işlendikten sonra işler ve bir çerçevede birden çok tıklamayı önlediğini unutmayın.


4

RxJava kullanarak Benzer Çözüm

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

3

Bir Handlerden dayalı throttler Sinyal 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);
    }
}

Örnek kullanım:

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

Örnek kullanım OnClickListener:

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

Örnek Kt kullanımı:

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

Veya bir uzantı ile:

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

Sonra örnek kullanım:

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

2

Bu amaçla Rxbinding3'ü kullanabilirsiniz. Sadece bu bağımlılığı build.gradle'a ekleyin

build.gradle

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

Daha sonra aktivitenizde veya parçanızda aşağıdaki kodu kullanın

your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe {
    // your action
}

1

Bu sınıfı veri bağlama ile birlikte kullanıyorum. Harika çalışıyor.

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

Ve düzenim şöyle görünüyor

<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}" />

1

Bu senaryoyu ele almanın daha önemli bir yolu, RxJava2 ile Kısma operatörünü (Önce Gaz Verme) kullanmaktır. Kotlin'de bunu başarmanın adımları:

1). Bağımlılıklar: - build.gradle uygulama seviyesi dosyasında rxjava2 bağımlılığı ekleyin.

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

2). View.OnClickListener uygulayan ve görünümün OnClick () yöntemini işlemek için ilk çalıştırıcıyı içeren soyut bir sınıf oluşturun. Kod pasajı şu şekildedir:

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). Bu SingleClickListener sınıfını etkinlikteki görünüm tıklamasına uygulayın. Bu şu şekilde elde edilebilir:

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

Yukarıdaki adımları uygulamaya uygulamak, bir görünümde 600mS'ye kadar birden fazla tıklamayı basitçe önleyebilir. Mutlu kodlamalar!


0

Bu böyle çözüldü

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

0

İşte lambdalarla kullanılabilecek oldukça basit bir çözüm :

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

İşte kopyala / yapıştır hazır 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);
    }
}

Zevk almak!


0

Dayanarak @GreyBeardedGeek cevap ,

  • debounceClick_last_TimestampÜzerinde oluşturids.xmlÖnceki tıklama zaman damgasını etiketlemek için .
  • Bu kod bloğunu şuraya ekle: 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);
    }
    
  • Kullanım:

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

    view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
    

0

Buraya küçük bir örnek verin

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}

1
lütfen
RxView'unuzun

0

removeallÇözümüm, parçadan ve faaliyetten çıktığımızda (yok ettiğimizde) şunu aramalıyız :

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

0

En kolay yolun KProgressHUD gibi bir "yükleme" kitaplığı kullanmak olduğunu söyleyebilirim.

https://github.com/Kaopiz/KProgressHUD

OnClick yöntemindeki ilk şey, geliştirici onu serbest bırakmaya karar verene kadar tüm kullanıcı arayüzünü anında engelleyen yükleme animasyonunu çağırmak olacaktır.

Yani, onClick işlemi için buna sahip olursunuz (bu, Butterknife kullanır, ancak açıkça her tür yaklaşımla çalışır):

Ayrıca, tıklamadan sonra düğmeyi devre dışı bırakmayı unutmayın.

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

Sonra:

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

Ve isterseniz, stopHUDSpinner yöntemini doAction () yönteminde kullanabilirsiniz:

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

Düğmeyi uygulama mantığınıza göre yeniden etkinleştirin: button.setEnabled (true);

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.