your programing

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

lovepro 2020. 10. 13. 08:18
반응형

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


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

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

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

도와주세요 감사합니다


여기 내가 최근에 쓴 '디 바운스 된'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.gradleRxBinding 종속성을 추가하려면 다음 줄 을 추가하십시오.

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

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


다음은 간단한 예입니다.

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 기기에서 시간을 변경하여 과거로 되돌릴 수 있으므로 이것이 없으면 버그가 발생할 수 있습니다.

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


클릭뿐 아니라 모든 이벤트에서 작동하는 것이 있습니다. 또한 일련의 빠른 이벤트 (예 : 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의 스팸 메서드를 호출합니다.


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

이 클래스를 데이터 바인딩과 함께 사용합니다. 잘 작동합니다.

/**
 * 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}" />

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

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

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
}

이것은 이렇게 해결됩니다

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

So you would have this for the onClick action (this uses Butterknife but it obviously works with any kind of approach):

Also, don't forget to disable the button after the click.

@OnClick(R.id.button)
void didClickOnButton() {
    startHUDSpinner();
    button.setEnabled(false);
    doAction();
}

Then:

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

And you can use the stopHUDSpinner method in the doAction() method if you so desire:

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

Re-enable the button according to your app logic: button.setEnabled(true);

참고URL : https://stackoverflow.com/questions/16534369/avoid-button-multiple-rapid-clicks

반응형