Hindari tombol beberapa klik cepat

88

Saya memiliki masalah dengan aplikasi saya yang jika pengguna mengklik tombol beberapa kali dengan cepat, maka beberapa peristiwa dihasilkan bahkan sebelum dialog saya menahan tombol tersebut menghilang

Saya tahu solusi dengan menetapkan variabel boolean sebagai bendera ketika tombol diklik sehingga klik di masa mendatang dapat dicegah hingga dialog ditutup. Namun saya memiliki banyak tombol dan harus melakukan ini setiap saat karena setiap tombol tampaknya berlebihan. Apakah tidak ada cara lain di android (atau mungkin solusi yang lebih cerdas) untuk mengizinkan hanya tindakan peristiwa yang dihasilkan per klik tombol?

Yang lebih buruk lagi adalah bahwa beberapa klik cepat tampaknya menghasilkan beberapa tindakan peristiwa bahkan sebelum tindakan pertama ditangani, jadi jika saya ingin menonaktifkan tombol dalam metode penanganan klik pertama, sudah ada tindakan peristiwa dalam antrian yang menunggu untuk ditangani!

Tolong bantu Terima kasih

Ular
sumber
dapatkah kita menerapkan kode menggunakan kode GreyBeardedGeek. Saya tidak yakin bagaimana Anda menggunakan kelas abstrak dalam Aktivitas Anda?
CoDe
Coba solusi ini juga stackoverflow.com/questions/20971484/…
DmitryBoyko
Saya memecahkan masalah serupa menggunakan tekanan balik di RxJava
arekolek

Jawaban:

114

Berikut adalah listener onClick 'debounce' yang saya tulis baru-baru ini. Anda memberi tahu berapa jumlah minimum milidetik yang dapat diterima di antara klik. Terapkan logika Anda onDebouncedClicksebagai gantionClick

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
sumber
3
Oh, seandainya setiap situs web mau repot menerapkan sesuatu seperti ini! Tidak ada lagi "jangan klik kirim dua kali atau Anda akan dikenakan biaya dua kali"! Terima kasih.
Floris
2
Sejauh yang saya tahu, tidak. OnClick harus selalu terjadi di thread UI (hanya ada satu). Jadi, beberapa klik, meski waktunya berdekatan, harus selalu berurutan, di utas yang sama.
GreyBeardedGeek
45
@FengDai Menarik - "kekacauan" ini melakukan apa yang perpustakaan Anda lakukan, sekitar 1/20 dari jumlah kode. Anda memiliki solusi alternatif yang menarik, tetapi ini bukan tempat untuk menghina kode yang berfungsi.
GreyBeardedGeek
13
@GreyBeardedGeek Maaf telah menggunakan kata yang buruk itu.
Feng Dai
2
Ini adalah pelambatan, bukan pelepasan.
Pemberi hadiah
71

Dengan RxBinding itu bisa dilakukan dengan mudah. Berikut ini contohnya:

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

Tambahkan baris berikut build.gradleuntuk menambahkan dependensi RxBinding:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
sumber
2
Keputusan terbaik. Perhatikan bahwa ini menahan tautan kuat ke tampilan Anda sehingga fragmen tidak akan dikumpulkan setelah siklus proses yang biasa selesai - gabungkan ke dalam pustaka Siklus Hidup Trello. Kemudian akan dihentikan langganannya dan tampilan dibebaskan setelah metode siklus hidup yang dipilih dipanggil (biasanya onDestroyView)
Den Drobiazko
2
Ini tidak berfungsi dengan adaptor, masih dapat mengklik beberapa item, dan ini akan melakukan throttleFirst tetapi pada setiap tampilan individual.
desgraci
1
Dapat diimplementasikan dengan Handler dengan mudah, tidak perlu menggunakan RxJava di sini.
Miha_x64
20

Ini versi saya dari jawaban yang diterima. Ini sangat mirip, tetapi tidak mencoba untuk menyimpan Tampilan dalam Peta yang menurut saya bukan ide yang bagus. Ini juga menambahkan metode bungkus yang dapat berguna dalam banyak situasi.

/**
 * 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
sumber
Bagaimana cara menggunakan metode bungkus? Bisakah Anda memberi contoh. Terima kasih.
Mehul Joisar
1
@MehulJoisar Seperti ini:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD
11

Kami dapat melakukannya tanpa perpustakaan apa pun. Buat saja satu fungsi ekstensi:

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

Lihat onClick menggunakan kode di bawah ini:

buttonShare.clickWithDebounce { 
   // Do anything you want
}
SANAT
sumber
Ini keren! Padahal menurut saya istilah yang lebih tepat disini adalah "throttle" bukan "debounce". yaitu clickWithThrottle ()
hopia
private var lastClickTime: Long = 0harus dipindahkan dari setOnClickListener {...} atau lastClickTime akan selalu 0.
Robert
9

Anda dapat menggunakan proyek ini: https://github.com/fengdai/clickguard untuk menyelesaikan masalah ini dengan satu pernyataan:

ClickGuard.guard(button);

UPDATE: Perpustakaan ini tidak lagi direkomendasikan. Saya lebih suka solusi Nikita. Gunakan RxBinding sebagai gantinya.

Feng Dai
sumber
@ Feng Dai, bagaimana menggunakan perpustakaan ini untukListView
CAMOBAP
Bagaimana cara menggunakan ini di studio android?
VVB
Perpustakaan ini tidak lagi direkomendasikan. Silakan gunakan RxBinding sebagai gantinya. Lihat jawaban Nikita.
Feng Dai
7

Berikut contoh sederhananya:

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
sumber
1
sampel dan bagus!
Chulo
5

Hanya pembaruan cepat pada solusi GreyBeardedGeek. Ubah klausa if dan tambahkan fungsi Math.abs. Atur seperti ini:

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

Pengguna dapat mengubah waktu di perangkat Android dan menyimpannya di masa lalu, jadi tanpa ini dapat menyebabkan bug.

PS: Tidak ada cukup poin untuk mengomentari solusi Anda, jadi saya hanya memberikan jawaban lain.

NixSam
sumber
5

Ini adalah sesuatu yang akan berfungsi dengan acara apa pun, bukan hanya klik. Ini juga akan memberikan acara terakhir meskipun itu bagian dari rangkaian peristiwa cepat (seperti 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) Buat Debouncer di bidang pendengar tetapi di luar fungsi panggilan balik, dikonfigurasi dengan batas waktu dan panggilan balik fn yang ingin Anda throttle.

2) Panggil metode spam Debouncer Anda dalam fungsi callback pendengar.

vlazzle.dll
sumber
5

Jadi, jawaban ini disediakan oleh perpustakaan 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);
}

Metode ini menangani klik hanya setelah klik sebelumnya ditangani dan perhatikan bahwa ini menghindari beberapa klik dalam satu bingkai.

Piyush Jain
sumber
4

Solusi serupa menggunakan 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
sumber
3

Sebuah Handlerthrottler berdasarkan dari 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);
    }
}

Contoh penggunaan:

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

Contoh penggunaan di OnClickListener:

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

Contoh penggunaan Kt:

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

Atau dengan ekstensi:

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

Kemudian contoh penggunaan:

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

Saya menggunakan kelas ini bersama dengan penyatuan data. Bekerja dengan baik.

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

Dan tata letak saya terlihat seperti ini

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

Cara yang lebih signifikan untuk menangani skenario ini adalah menggunakan operator Throttling (Throttle First) dengan RxJava2. Langkah-langkah untuk mencapai ini di Kotlin:

1). Dependensi: - Tambahkan dependensi rxjava2 di file level aplikasi build.gradle.

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

2). Buat kelas abstrak yang mengimplementasikan View.OnClickListener & berisi operator pertama throttle untuk menangani metode OnClick () tampilan. Cuplikan kode adalah sebagai:

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). Implementasikan kelas SingleClickListener ini dengan mengklik tampilan dalam aktivitas. Ini dapat dicapai sebagai:

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

Menerapkan langkah-langkah di atas ke dalam aplikasi dapat dengan mudah menghindari beberapa klik pada tampilan hingga 600mS. Selamat membuat kode!

Abhijeet
sumber
1

Anda dapat menggunakan Rxbinding3 untuk tujuan itu. Cukup tambahkan dependensi ini di build.gradle

build.gradle

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

Kemudian dalam aktivitas atau fragmen Anda, gunakan kode di bawah ini

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

Ini diselesaikan seperti ini

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

Berikut ini solusi yang cukup sederhana, yang dapat digunakan dengan lambda:

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

Berikut cuplikan siap salin / tempel:

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

Nikmati!

Leo Droidcoder
sumber
0

Berdasarkan jawaban @GreyBeardedGeek ,

  • Buat debounceClick_last_Timestamppada ids.xmlke tag timestamp klik sebelumnya.
  • Tambahkan blok kode ini ke dalam 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);
    }
    
  • Pemakaian:

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

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

Berikan sedikit contoh di sini

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}
Shwarz Andrei
sumber
1
berikan tautan atau deskripsi apa RxView Anda
BekaBot
0

Solusi saya, perlu memanggil removeallketika kita keluar (menghancurkan) dari fragmen dan aktivitas:

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)
        }
    }
pengguna3748333
sumber
0

Saya akan mengatakan cara termudah adalah dengan menggunakan perpustakaan "memuat" seperti KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

Hal pertama pada metode onClick adalah memanggil animasi pemuatan yang langsung memblokir semua UI sampai pengembang memutuskan untuk membebaskannya.

Jadi Anda akan memiliki ini untuk tindakan onClick (ini menggunakan Butterknife tetapi jelas berfungsi dengan segala jenis pendekatan):

Juga, jangan lupa untuk menonaktifkan tombol setelah klik.

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

Kemudian:

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

Dan Anda bisa menggunakan metode stopHUDSpinner dalam metode doAction () jika Anda menginginkan:

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

Aktifkan kembali tombol sesuai dengan logika aplikasi Anda: button.setEnabled (true);

Alex Capitaneanu
sumber