ボタンの複数回の高速クリックを避ける


86

ユーザーがボタンをすばやく複数回クリックすると、ボタンを押しているダイアログが消える前に複数のイベントが生成されるというアプリの問題があります

ボタンがクリックされたときにブール変数をフラグとして設定することで解決策を知っているので、ダイアログが閉じるまで今後のクリックを防ぐことができます。しかし、私にはたくさんのボタンがあり、すべてのボタンに対して毎回これをしなければならないのはやり過ぎのようです。ボタンのクリックごとに生成されたイベントアクションのみを許可するAndroid(またはよりスマートなソリューション)の他の方法はありませんか?

さらに悪いことに、最初のアクションが処理される前に複数のクイッククリックが複数のイベントアクションを生成するように見えるため、最初のクリック処理方法でボタンを無効にしたい場合は、処理を待機しているキューに既存のイベントアクションがあります!

助けてくださいありがとう


GreyBeardedGeekコードを使用してコードを実装できますか?urアクティビティで抽象クラスをどのように使用したかわかりませんか?
CoDe 2015

よく、このソリューションをお試しくださいstackoverflow.com/questions/20971484/...
DmitryBoyko

回答:


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回クリックしないでください。クリックすると、2回請求されます」。ありがとうございました。
フローリス2013年

2
私の知る限りではありません。onClickは常にUIスレッドで発生する必要があります(1つだけです)。したがって、複数回のクリックは、時間的には近いものの、同じスレッド上で常に連続している必要があります。
GreyBeardedGeek 2013年

45
@FengDai興味深い-この「混乱」は、コード量の約1/20で、ライブラリが行うことを実行します。興味深い代替ソリューションがありますが、これは動作中のコードを侮辱する場所ではありません。
GreyBeardedGeek 2015

13
@GreyBeardedGeekその悪い言葉を使ってすみません。
Feng Dai

2
これは抑制であり、デバウンスではありません。
Rewieer

71

RxBindingそれが簡単に行うことができます。次に例を示します。

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

build.gradleRxBinding依存関係を追加するには、次の行を追加します。

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

2
最良の決定。これはビューとの強いリンクを保持しているため、通常のライフサイクルが完了した後にフラグメントが収集されないことに注意してください。Trelloのライフサイクルライブラリにラップしてください。次に、選択したライフサイクルメソッドが呼び出された後(通常はonDestroyView)、サブスクライブが解除され、ビューが解放されます
Den Drobiazko 2016年

2
これはアダプタでは機能しませんが、複数のアイテムをクリックすることができ、throttleFirstを実行しますが、個々のビューで実行します。
desgraci 2016年

1
Handlerで簡単に実装でき、ここでRxJavaを使用する必要はありません。
miha_x64 2018

20

これが私のバージョンの受け入れられた答えです。これは非常に似ていますが、ビューをマップに保存しようとはしません。これはあまり良い考えではないと思います。また、多くの状況で役立つ可能性のあるwrapメソッドも追加されます。

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

ラップ方式の使い方は?例を挙げていただけますか。ありがとうございます。
Mehul Joisar 2017

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()
hopia19年

9

このプロジェクトを使用できます:https//github.com/fengdai/clickguard は、次の1つのステートメントでこの問題を解決します

ClickGuard.guard(button);

更新:このライブラリは推奨されなくなりました。私はニキータのソリューションを好みます。代わりにRxBindingを使用してください。


@Feng Dai、このライブラリの使用方法ListView
CAMOBAP 2015

これをAndroidStudioで使用するにはどうすればよいですか?
VVB 2016年

このライブラリはもうお勧めしません。代わりに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
サンプルといいね!
Chulo

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)リスナーのフィールドで、コールバック関数の外側で、タイムアウトとスロットルするコールバックfnで構成されたDebouncerを構築します。

2)リスナーのコールバック関数でDebouncerのspamメソッドを呼び出します。


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ベースのスロットラー信号の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でスロットル演算子(スロットルファースト)を使用することです。Kotlinでこれを実現するための手順:

1)。依存関係:-build.gradleアプリレベルファイルにrxjava2依存関係を追加します。

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

上記の手順をアプリに実装すると、600mSまでビューを何度もクリックすることを簡単に回避できます。ハッピーコーディング!


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_Timestampids.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メソッドの最初のことは、開発者がUIを解放することを決定するまで、すべてのUIを即座にブロックする読み込みアニメーションを呼び出すことです。

したがって、onClickアクションにはこれがあります(これはバターナイフを使用しますが、明らかにあらゆる種類のアプローチで機能します):

また、クリック後にボタンを無効にすることを忘れないでください。

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

また、必要に応じて、doAction()メソッドでstopHUDSpinnerメソッドを使用できます。

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

アプリのロジックに従ってボタンを再度有効にします。button.setEnabled(true);

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.