Избегайте кнопки несколько быстрых щелчков
У меня проблема с моим приложением, что если пользователь нажимает кнопку несколько раз быстро, то несколько событий генерируются до того, как даже мой диалог, удерживающий кнопку, исчезнет
Я знаю решение, установив логическую переменную в качестве флага при нажатии кнопки, чтобы будущие щелчки можно было предотвратить до закрытия диалогового окна. Однако у меня есть много кнопок и делать это каждый раз для каждой кнопки, кажется, перебор. Нет ли другого способа в android (или, может быть какое-то более умное решение), чтобы разрешить только только действие события, созданное за один клик кнопки?
Что еще хуже, так это то, что несколько быстрых кликов, похоже, генерируют несколько действий события до того, как будет обработано первое действие, поэтому, если я хочу отключить кнопку в методе обработки первого клика, в очереди уже есть действия событий, ожидающие обработки!
пожалуйста, помогите Спасибо
11 ответов:
вот "дебошир" 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 minimumInterval; 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 minimumIntervalMsec The minimum allowed time between clicks - any click sooner than this after a previous click will be rejected */ public DebouncedOnClickListener(long minimumIntervalMsec) { this.minimumInterval = minimumIntervalMsec; this.lastClickMap = new WeakHashMap<View, Long>(); } @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.longValue()) > minimumInterval) { onDebouncedClick(clickedView); } } }
С RxBinding Это можно легко сделать. Вот пример:
RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> { // action on click });
добавить следующую строку
build.gradle
чтобы добавить зависимость RxBinding:compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
вот моя версия принятого ответа. Это очень похоже, но не пытается хранить виды на карте, которая я не думаю, что это такая хорошая идея. Он также добавляет метод обертывания, который может быть полезен во многих ситуациях.
/** * 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); } }; } }
вы можете использовать этот проект: https://github.com/fengdai/clickguard чтобы решить эту проблему с помощью одного оператора:
ClickGuard.guard(button);
обновление: эта библиотека больше не рекомендуется. Я предпочитаю решение Никиты. Вместо этого используйте RxBinding.
просто быстрое обновление решения GreyBeardedGeek. Измените предложение if и добавьте математику.функция abs. Установите его следующим образом:
if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) { onDebouncedClick(clickedView); }
пользователь может изменить время на Android устройстве и положить его в прошлое, так что без этого это может привести к ошибке.
PS: не хватает очков, чтобы комментировать ваше решение, поэтому я просто поставил другой ответ.
вот простой пример:
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); }
аналогичное решение с использованием 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); }
вот то, что будет работать с любым событием, а не только щелчки. Он также доставит последнее событие, даже если это часть серии быстрых событий (например,ГХ дребезга).
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) вызовите метод спама вашего Дебаунсера в функции обратного вызова слушателя.
это решается так
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()); } });
Я использую этот класс вместе с привязки данных. Отлично работает.
/** * 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}" />
вот довольно простое решение, который можно использовать с лямбдами:
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); } }
наслаждайтесь!