Избегайте многократных быстрых нажатий кнопки

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 05
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 01
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))
директоров
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 ()
Hopia
9

Вы можете использовать этот проект: https://github.com/fengdai/clickguard, чтобы решить эту проблему с помощью одного оператора:

ClickGuard.guard(button);

ОБНОВЛЕНИЕ: эта библиотека больше не рекомендуется. Я предпочитаю решение Никиты. Вместо этого используйте RxBinding.

Фэн Дай
источник
@Feng Dai, как использовать эту библиотеку дляListView
CAMOBAP
Как использовать это в студии Android?
VVB
Эта библиотека больше не рекомендуется. Вместо этого используйте RxBinding. См. Ответ Никиты.
Feng Dai
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: недостаточно очков, чтобы прокомментировать ваше решение, поэтому я просто поставил другой ответ.

NixSam
источник
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 в поле слушателя, но вне функции обратного вызова, настроенного с тайм-аутом и функцией обратного вызова, которую вы хотите ограничить.

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

Более значительный способ справиться с этим сценарием - использовать оператор регулирования (сначала дросселирование) с 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)
        }
    }
пользователь3748333
источник
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);

Алекс Капитаняну
источник