Уникайте кількох швидких натискань кнопки


86

У мене є проблема з моїм додатком: якщо користувач швидко натискає кнопку кілька разів, генеруються кілька подій, перш ніж зникне навіть моє діалогове вікно, що утримує кнопку

Я знаю рішення, встановивши логічну змінну як прапор при натисканні кнопки, щоб можна було запобігти майбутнім натисканням, доки діалогове вікно не буде закрито. Однак у мене багато кнопок, і доводиться робити це щоразу для кожної кнопки, здається, надмірно. Чи немає іншого способу в android (або, можливо, більш розумного рішення) дозволити лише дію події, що генерується за клік кнопки?

Ще гірше те, що кілька швидких клацань, здається, генерують кілька дій події ще до того, як обробляється навіть перша дія, тому, якщо я хочу відключити кнопку в методі обробки першого клацання, у черзі вже існують дії подій, які очікують на обробку!

Будь ласка, допоможіть Спасибі


чи можемо ми застосувати код, що використовується кодом GreyBeardedGeek. Я не впевнений, як ти використовував абстрактний клас у ur Activity?
CoDe

Спробуйте це рішення , а також stackoverflow.com/questions/20971484 / ...
DmitryBoyko

Я вирішив подібну проблему за допомогою протитиску в RxJava
arekolek

Відповіді:


111

Ось "розбещений" слухач onClick, який я нещодавно писав. Ви скажете, яка мінімально допустима кількість мілісекунд між кліками. Впроваджуйте свою логіку onDebouncedClickзамістьonClick

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
О, якби тільки кожен веб-сайт міг турбуватися, впроваджуючи щось подібне! Більше "не натискайте два рази" подати, інакше з вас буде стягнено двічі "! Дякую.
Флоріс

2
Наскільки мені відомо, ні. OnClick завжди повинен відбуватися в потоці інтерфейсу користувача (існує лише один). Отже, кілька кліків, хоча і близькі за часом, завжди повинні бути послідовними в одному потоці.
GreyBeardedGeek

45
@FengDai Цікаво - ця "каша" робить те, що робить ваша бібліотека, приблизно на 1/20 від суми коду. У вас є цікаве альтернативне рішення, але тут не місце ображати робочий код.
GreyBeardedGeek

13
@GreyBeardedGeek Вибачте за вживання цього поганого слова.
Фен Дай

2
Це регулювання, а не розвінчання.
Rewieer

71

За допомогою RxBinding це можна зробити легко. Ось приклад:

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

Додайте наступний рядок, build.gradleщоб додати залежність RxBinding:

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

2
Найкраще рішення. Тільки зауважте, що це містить міцне посилання на ваш погляд, тому фрагмент не буде зібраний після завершення звичайного життєвого циклу - оберніть його в бібліотеку життєвого циклу Trello. Тоді він буде відписаний і перегляд звільнений після виклику вибраного методу життєвого циклу (зазвичай onDestroyView)
Ден Дроб'язко,

2
Це не працює з адаптерами, ви все одно можете натиснути на кілька елементів, і це буде робити throttleFirst, але на кожному окремому поданні.
desgraci

1
Легко реалізується за допомогою Handler, тут не потрібно використовувати RxJava.
Miha_x64

20

Ось моя версія прийнятої відповіді. Це дуже схоже, але не намагається зберігати подання на карті, що, на мою думку, не є такою гарною ідеєю. Він також додає метод обгортання, який може бути корисним у багатьох ситуаціях.

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

Як використовувати метод обгортання? Ви можете навести приклад, дякую.
Мехул Джойсар,

1
@MehulJoisar Ось так:myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
BoD

10

Ми можемо зробити це без будь-якої бібліотеки. Просто створіть одну функцію розширення:

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

Переглянути onClick, використовуючи код нижче:

buttonShare.clickWithDebounce { 
   // Do anything you want
}

Це круто! Хоча я вважаю, що тут найбільш підходящий термін - "дросель", а не "розрив". тобто clickWithThrottle ()
хопія

9

Ви можете використати цей проект: https://github.com/fengdai/clickguard, щоб вирішити цю проблему одним твердженням:

ClickGuard.guard(button);

ОНОВЛЕННЯ: Ця бібліотека більше не рекомендується. Я віддаю перевагу рішенням Микити. Замість цього використовуйте RxBinding.


@ Фен Дай, як користуватися цією бібліотекою дляListView
CAMOBAP

Як використовувати це в андроїд-студії?
VVB

Цю бібліотеку більше не рекомендується. Будь ласка, використовуйте RxBinding замість цього. Дивіться відповідь Микити.
Фен Дай

7

Ось простий приклад:

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
зразок і приємно!
Чуло

5

Просто коротке оновлення рішення GreyBeardedGeek. Змініть речення if і додайте функцію Math.abs. Встановіть це так:

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

Користувач може змінити час на пристрої Android і поставити його в минуле, тому без цього це може призвести до помилки.

PS: не вистачає балів, щоб прокоментувати ваше рішення, тому я просто даю іншу відповідь.


5

Ось щось, що буде працювати з будь-якою подією, а не лише з кліками. Він також доставить останню подію, навіть якщо це частина серії швидких подій (наприклад, 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) Побудуйте Debouncer у полі слухача, але за межами функції зворотного виклику, налаштованої з часом очікування та зворотним викликом fn, який потрібно зменшити.

2) Викличте метод спаму вашого Debouncer у функції зворотного дзвінка слухача.


4

Подібне рішення з використанням 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);
}

4

Отже, цю відповідь надає бібліотека 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);
}

Цей метод обробляє клацання лише після того, як було оброблено попередній клік, і зверніть увагу, що він дозволяє уникнути кількох клацань у кадрі.


3

HandlerОснові throttler з сигналу 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);
    }
}

Приклад використання:

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

Приклад використання в OnClickListener:

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

Приклад використання Kt:

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

Або з розширенням:

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

Тоді приклад використання:

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

1

Я використовую цей клас разом із прив'язкою даних. Чудово працює.

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

А мій макет виглядає так

<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

Більш важливим способом обробки цього сценарію є використання оператора Throttling (Throttle First) з RxJava2. Кроки для досягнення цього в Котліні:

1). Залежності: - Додайте залежність rxjava2 у файл додатку build.gradle.

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

2). Побудуйте абстрактний клас, який реалізує View.OnClickListener і містить перший оператор дросельної заслінки, який обробляє метод OnClick () подання. Фрагмент коду такий:

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). Реалізуйте цей клас SingleClickListener за натисканням подання в діяльності. Цього можна досягти як:

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

Реалізація цих вищевказаних кроків у програму дозволяє просто уникнути багаторазових клацань на поданні до 600 мС. Щасливого кодування!


1

Для цього ви можете використовувати Rxbinding3. Просто додайте цю залежність у build.gradle

build.gradle

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

Потім у своїй діяльності або фрагменті використовуйте нижченаведений код

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

0

Це вирішується так

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

Ось досить просте рішення, яке можна використовувати з лямбдами:

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

Ось готовий фрагмент копіювання / вставлення:

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

Насолоджуйтесь!


0

На основі відповіді @GreyBeardedGeek ,

  • Створення debounceClick_last_Timestampна ids.xmlпозначати попередній клацання мітку.
  • Додайте цей блок коду в 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);
    }
    
  • Використання:

    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
                }
            });
        }
    });
    
  • Використання лямбди

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

0

Наведіть тут невеликий приклад

view.safeClick { doSomething() }

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

1
будь-ласка, надайте посилання або опис, яким є ваш RxView
BekaBot

0

Моє рішення, потрібно зателефонувати, removeallколи ми виходимо (знищуємо) з фрагмента та активності:

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

Я б сказав, що найпростіший спосіб - це використовувати "завантажувальну" бібліотеку, як KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

Першим ділом методу onClick було б викликати анімацію завантаження, яка миттєво блокує весь інтерфейс, поки розробник не вирішить його звільнити.

Отже, ви мали б це для дії onClick (тут використовується Butterknife, але це, очевидно, працює з будь-яким підходом):

Також не забудьте відключити кнопку після натискання.

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

Тоді:

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

І ви можете використовувати метод stopHUDSpinner у методі doAction (), якщо забажаєте:

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

Повторно ввімкніть кнопку відповідно до вашої логіки програми: button.setEnabled (true);

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.