У меня проблема с моим приложением: если пользователь нажимает кнопку несколько раз быстро, генерируются несколько событий, прежде чем даже мое диалоговое окно с кнопкой исчезнет.
Я знаю решение, устанавливая логическую переменную в качестве флага при нажатии кнопки, чтобы можно было предотвратить будущие щелчки, пока диалоговое окно не будет закрыто. Однако у меня много кнопок, и необходимость делать это каждый раз для каждой кнопки кажется излишним. Нет ли другого способа в Android (или, может быть, более разумного решения), чтобы разрешить только действие события, генерируемое при нажатии кнопки?
Что еще хуже, несколько быстрых щелчков, кажется, генерируют несколько действий по событию до того, как будет обработано даже первое действие, поэтому, если я хочу отключить кнопку в методе обработки первого щелчка, в очереди уже есть действия с событиями, ожидающие обработки!
Пожалуйста, помогите Спасибо
Ответы:
Вот недавно написанный мной обработанный прослушиватель 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); } } }
источник
С 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); } }; } }
источник
myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
Мы можем сделать это без какой-либо библиотеки. Просто создайте одну единственную функцию расширения:
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 }
источник
Вы можете использовать этот проект: https://github.com/fengdai/clickguard, чтобы решить эту проблему с помощью одного оператора:
ClickGuard.guard(button);
ОБНОВЛЕНИЕ: эта библиотека больше не рекомендуется. Я предпочитаю решение Никиты. Вместо этого используйте RxBinding.
источник
ListView
Вот простой пример:
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); }
источник
Просто быстрое обновление решения GreyBeardedGeek. Измените предложение if и добавьте функцию Math.abs. Установите это так:
if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) { onDebouncedClick(clickedView); }
Пользователь может изменить время на устройстве Android и вернуть его в прошлое, так что без этого это может привести к ошибке.
PS: недостаточно очков, чтобы прокомментировать ваше решение, поэтому я просто поставил другой ответ.
источник
Вот то, что будет работать с любым событием, а не только с кликами. Он также доставит последнее событие, даже если оно является частью серии быстрых событий (например, 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 в поле слушателя, но вне функции обратного вызова, настроенного с тайм-аутом и функцией обратного вызова, которую вы хотите ограничить.
2) Вызовите метод спама вашего Debouncer в функции обратного вызова слушателя.
источник
Аналогичное решение с использованием 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); }
источник
Итак, этот ответ дает библиотека 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); }
Этот метод обрабатывает щелчки только после того, как был обработан предыдущий щелчок, и обратите внимание, что он позволяет избежать множественных щелчков в кадре.
источник
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") }
источник
Я использую этот класс вместе с привязкой данных. Работает отлично.
/** * 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}" />
источник
Более значительный способ справиться с этим сценарием - использовать оператор регулирования (сначала дросселирование) с 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 мс. Удачного кодирования!
источник
Для этой цели вы можете использовать Rxbinding3. Просто добавьте эту зависимость в build.gradle
build.gradle
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
Затем в своей деятельности или фрагменте используйте приведенный ниже код
your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe { // your action }
источник
Это решается так
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()); } });
источник
Вот довольно простое решение, которое можно использовать с лямбдами:
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); } }
Наслаждайтесь!
источник
На основе ответа @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));
источник
Поместите здесь небольшой пример
view.safeClick { doSomething() }
@SuppressLint("CheckResult") fun View.safeClick(invoke: () -> Unit) { RxView .clicks(this) .throttleFirst(300, TimeUnit.MILLISECONDS) .subscribe { invoke() } }
источник
Мое решение, нужно вызывать,
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) } }
источник
Я бы сказал, что самый простой способ - использовать «загрузочную» библиотеку вроде 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);
источник