버튼을 여러 번 빠르게 클릭하지 마십시오.


88

사용자가 버튼을 빠르게 여러 번 클릭하면 버튼을 누른 대화 상자도 사라지기 전에 여러 이벤트가 생성된다는 문제가 앱에 있습니다.

버튼을 클릭 할 때 부울 변수를 플래그로 설정하여 대화 상자가 닫힐 때까지 향후 클릭을 방지 할 수있는 솔루션을 알고 있습니다. 그러나 나는 많은 버튼을 가지고 있으며 모든 버튼에 대해 매번 이것을 해야하는 것은 과도한 것 같습니다. 버튼 클릭당 생성 된 이벤트 액션 만 허용하는 다른 방법이 안드로이드 (또는 더 스마트 한 솔루션)에 없습니까?

더 나쁜 것은 여러 번의 빠른 클릭이 첫 번째 작업이 처리되기 전에 여러 이벤트 작업을 생성하는 것처럼 보이므로 첫 번째 클릭 처리 방법에서 버튼을 비활성화하려면 대기열에 이미 처리 대기중인 이벤트 작업이 있습니다!

도와주세요 감사합니다


GreyBeardedGeek 코드를 사용하는 코드를 구현할 수 있습니까? ur Activity에서 추상 클래스를 어떻게 사용했는지 잘 모르겠습니까?
CoDe 2015

잘으로이 솔루션을 시도 stackoverflow.com/questions/20971484/...
DmitryBoyko

답변:


114

여기 내가 최근에 쓴 '디 바운스 된'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
오, 모든 웹 사이트가 이러한 라인을 따라 무언가를 구현하는 것을 귀찮게 할 수 있다면! 더 이상 "제출을 두 번 클릭하지 마십시오. 그렇지 않으면 두 번 청구됩니다." 감사합니다.
Floris 2013 년

2
내가 아는 한, 아니. onClick은 항상 UI 스레드에서 발생해야합니다 (하나만 있음). 따라서 시간이 가까워 지더라도 여러 번의 클릭은 항상 동일한 스레드에서 순차적이어야합니다.
GreyBeardedGeek

45
@FengDai 흥미 롭습니다-이 "mess"는 코드 양의 약 1/20으로 라이브러리가하는 일을합니다. 흥미로운 대안 솔루션이 있지만 이것은 작업 코드를 모욕하는 곳이 아닙니다.
GreyBeardedGeek

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

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

랩 방식은 어떻게 사용하나요? 예를 들어 주시겠습니까? 감사합니다.
Mehul Joisar

1
@MehulJoisar처럼이 :myButton.setOnClickListener(OnSingleClickListener.wrap(myOnClickListener))
이사회

11

도서관 없이도 할 수 있습니다. 하나의 확장 기능을 만드십시오.

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

private var lastClickTime: Long = 0setOnClickListener {...} 밖으로 이동해야합니다. 그렇지 않으면 lastClickTime이 항상 0이됩니다.
Robert

9

https://github.com/fengdai/clickguard 프로젝트를 사용 하여 단일 문으로이 문제를 해결할 수 있습니다 .

ClickGuard.guard(button);

업데이트 :이 라이브러리는 더 이상 권장되지 않습니다. 나는 Nikita의 솔루션을 선호합니다. 대신 RxBinding을 사용하십시오.


@Feng 다이는 방법은이 라이브러리를 사용하는ListView
CAMOBAP

안드로이드 스튜디오에서 어떻게 사용하나요?
VVB

이 라이브러리는 더 이상 권장되지 않습니다. 대신 RxBinding을 사용하십시오. Nikita의 답변을 참조하십시오.
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
샘플과 좋은!
Chulo

5

GreyBeardedGeek 솔루션에 대한 간단한 업데이트입니다. if 절을 변경하고 Math.abs 함수를 추가합니다. 다음과 같이 설정하십시오.

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

사용자는 Android 기기에서 시간을 변경하여 과거로 전환 할 수 있으므로 이것이 없으면 버그로 이어질 수 있습니다.

추신 : 귀하의 솔루션에 대해 언급 할 포인트가 충분하지 않으므로 다른 답변을 드리겠습니다.


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의 스팸 메서드를 호출합니다.


5

따라서이 답변은 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);
}

이 메서드는 이전 클릭이 처리 된 후에 만 ​​클릭을 처리하며 한 프레임에서 여러 번 클릭하는 것을 방지합니다.


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

3

Signal AppHandler기반 스로틀 러 .

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

2

이를 위해 Rxbinding3을 사용할 수 있습니다. 이 종속성을 build.gradle에 추가하십시오.

build.gradle

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

그런 다음 활동 또는 조각에서 다음 코드를 사용하십시오.

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

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와 함께 Throttling 연산자 (Throttle First)를 사용하는 것입니다. 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) }
   }
}

삼). 활동에서보기를 클릭 할 때이 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까지보기를 여러 번 클릭하지 않아도됩니다. 즐거운 코딩 되세요!


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 메소드에서 가장 먼저해야 할 일은 개발자가 UI를 해제하기로 결정할 때까지 모든 UI를 즉시 차단하는 로딩 애니메이션을 호출하는 것입니다.

따라서 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;
}

그리고 원한다면 doAction () 메서드에서 stopHUDSpinner 메서드를 사용할 수 있습니다.

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

앱 로직에 따라 버튼을 다시 활성화합니다. button.setEnabled (true);

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.