Android如何防止按钮重复点击

235

如何在Android中防止按钮重复点击?


3
请参考qezt的解决方案,它是可行的。 - Henadzi Rabkin
3
应该接受qezt的解决方案,这样访问此页面的人就可以知道使用setEnabled(false)的缺点。 - LoveForDroid
你可以尝试使用我的库,它采用了最后点击时间的解决方案:https://github.com/RexLKW/SClick - Rex Lam
2
@Androider 请更改您的最佳答案。 - Amir133
57个回答

446

在点击时保存最后一次的点击时间可以避免这个问题。

即。

private long mLastClickTime = 0;

...

// inside onCreate or so:

findViewById(R.id.button).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        // mis-clicking prevention, using threshold of 1000 ms
        if (SystemClock.elapsedRealtime() - mLastClickTime < 1000){
            return;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        // do your magic here
    }
}

28
这是唯一一个真正可以防止双击的解决方案。虽然这个方法有点笨重,但我刚花了一个小时试了所有其他方法,但除了这个方法之外,没有一个能够防止快速双击视图。Google请修复这个问题 :) - ashishduh
7
这个解决方案很好。当屏幕上有多个按钮且您只想同时单击一个按钮时,此解决方案也适用。 - void
17
如果您双击足够快,这仍然不能完全防止误点两次。 - LHA
4
最佳答案应该在顶部。 - Henadzi Rabkin
3
这个解决方案应该是正确的答案...... setEnabled(false) 不足以解决问题。 - heloisasim
显示剩余18条评论

107

使用setEnabled(false)禁用按钮,直到对用户再次点击它是安全的。


57
setEnabled(false)似乎不够"快"。 我为一个按钮设置了View.OnClickListener.onClick(view)。在onClick()中,我首先写一个log语句,第二个语句是button.setEnabled(false)。当在模拟器中快速双击时,我看到log语句出现两次! 即使在此之后添加setClickable(false),log语句仍会出现两次。 - QQQuestions
108
我有一个类似的问题。根据一位热心绅士的说法,Android实际上会将点击事件排队,因此您的onClick()代码执行速度快慢并不重要,只有您点击按钮的速度重要。也就是说,您可能已经禁用了按钮,但是点击队列已经填满了两三个点击事件,禁用按钮对排队的点击事件没有影响。(尽管也许应该有影响?) - Nick
3
那难得的时刻,你看到“某人”给出了确切的答案,而不是抽象的回答问题... :P - Rahul
1
这并不总是正确的解决方案。如果onClick是昂贵的操作,那么按钮将无法快速禁用。在我看来,最完美的解决方案是使用上次点击时间,但也要禁用按钮,以避免用户再次轻敲它,特别是如果您期望操作需要一段时间(例如登录)。 - Sarang
4
您还应该使用view.cancelPendingInputEvents()来防止未捕获的点击。 - Maksim Turaev
显示剩余8条评论

66

我的解决方案是

package com.shuai.view;

import android.os.SystemClock;
import android.view.View;

/**
 * 处理快速在某个控件上双击2次(或多次)会导致onClick被触发2次(或多次)的问题
 * 通过判断2次click事件的时间间隔进行过滤
 * 
 * 子类通过实现{@link #onSingleClick}响应click事件
 */
public abstract class OnSingleClickListener implements View.OnClickListener {
    /**
     * 最短click事件的时间间隔
     */
    private static final long MIN_CLICK_INTERVAL=600;
    /**
     * 上次click的时间
     */
    private long mLastClickTime;

    /**
     * click响应函数
     * @param v The view that was clicked.
     */
    public abstract void onSingleClick(View v);

    @Override
    public final void onClick(View v) {
        long currentClickTime=SystemClock.uptimeMillis();
        long elapsedTime=currentClickTime-mLastClickTime;
        //有可能2次连击,也有可能3连击,保证mLastClickTime记录的总是上次click的时间
        mLastClickTime=currentClickTime;

        if(elapsedTime<=MIN_CLICK_INTERVAL)
            return;

        onSingleClick(v);        
    }

}

使用方式类似于OnClickListener,但是要重写onSingleClick()方法:

mTextView.setOnClickListener(new OnSingleClickListener() {
            @Override
            public void onSingleClick(View v) {
                if (DEBUG)
                    Log.i("TAG", "onclick!");
            }
     };

1
轻松完美地自动防止重复触摸。 - Benjamin Piette
1
如果您双击得足够快,这并不能完全防止双击。 - LHA
1
很棒的解决方案。我只是将MIN_CLICK_INTERVAL=1000改了一下。 - Nizam
8
如果我每0.5秒精确地按下按钮,由于每次点击都会更新mLastClickTime,我的后续点击将无法传递。我的建议是在检查时间间隔后再分配mLastClickTime。 - k2col
1
如果(elapsedTime<=MIN_CLICK_INTERVAL)的话,mLastClickTime=currentClickTime;应该放在return之后。 - Loyea
显示剩余4条评论

44

你可以使用Kotlin扩展函数RxBinding来以非常高端的方式完成这个任务。

   fun View.clickWithThrottle(throttleTime: Long = 600L, action: () -> Unit): Disposable =
        RxView.clicks(this)
                .throttleFirst(throttleTime, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe { action() }


fun View.clickWithThrottle(throttleTime: Long = 600L, action: () -> Unit) {
    this.setOnClickListener(object : View.OnClickListener {
        private var lastClickTime: Long = 0

        override fun onClick(v: View) {
            if (SystemClock.elapsedRealtime() - lastClickTime < throttleTime) return
            else action()

            lastClickTime = SystemClock.elapsedRealtime()
        }
    })
}

然后就只需要:

View.clickWithThrottle{ Your code }

过度工程化 - Serg Burlaka
5
Rx中的去抖动(Debounce)在这里不太适用。相反,“ThrottleFirst”应该更合适。 http://demo.nimius.net/debounce_throttle/ 这里有一个示例,展示它们之间的区别。PS:实际上,你的clickWithDebounce实现是节流(throttle)实现,而不是去抖动(debounce)。 - krossovochkin
这个方法能在数据绑定中使用吗? - Cyrus

44

如果在onClick()中执行计算密集型的工作,则仅禁用按钮或使其不可点击是不够的,因为点击事件可能会在按钮被禁用之前排队。我编写了一个实现OnClickListener接口的抽象基类,您可以覆盖它来解决此问题,该基类通过忽略任何排队的点击来解决此问题:

/** 
 * This class allows a single click and prevents multiple clicks on
 * the same button in rapid succession. Setting unclickable is not enough
 * because click events may still be queued up.
 * 
 * Override onOneClick() to handle single clicks. Call reset() when you want to
 * accept another click.
 */
public abstract class OnOneOffClickListener implements OnClickListener {
    private boolean clickable = true;

    /**
     * Override onOneClick() instead.
     */
    @Override
    public final void onClick(View v) {
        if (clickable) {
            clickable = false;
            onOneClick(v);
            //reset(); // uncomment this line to reset automatically
        }
    }

    /**
     * Override this function to handle clicks.
     * reset() must be called after each click for this function to be called
     * again.
     * @param v
     */
    public abstract void onOneClick(View v);

    /**
     * Allows another click.
     */
    public void reset() {
        clickable = true;
    }
}

使用方法与OnClickListener相同,只需覆盖OnOneClick()而不是onClick():

OnOneOffClickListener clickListener = new OnOneOffClickListener() {
    @Override
    public void onOneClick(View v) {

        // Do stuff

        this.reset(); // or you can reset somewhere else with clickListener.reset();
    }
};
myButton.setOnClickListener(clickListener);

感谢您提供的代码。根据您在这里编写的内容,我制作了一个类似的类,它可以防止在按钮或其父级隐藏时进行点击 - 有趣的是,即使您在单击后立即隐藏按钮,Android仍会排队等待点击。 - nikib3ro
1
我有一个类似的解决方案 - 你可以在xml中使用这个类,它会为你包装clickListeners https://github.com/doridori/AndroidUtilDump/blob/master/android/src/couk/doridori/android/lib/view/SafeButton.java - Dori
4
这是一个不错的解决方案,但需要对 onClick() 和 reset() 进行同步处理,否则仍然可能会出现双击的情况。 - Tom anMoney
7
@TomanMoney,怎么做呢?难道不是所有的点击事件都发生在单个UI线程上吗? - Ken
@Dori,链接已失效。 - naXa stands with Ukraine
@Ken 我猜Tom的意见是用户可能会从后台线程调用reset()方法,如果这个值没有在主线程中更新,那么用户就无法点击按钮(尽管也许应该可以)。 - Son Truong

33

Kotlin最简洁的惯用方式:

class OnSingleClickListener(private val block: () -> Unit) : View.OnClickListener {

    private var lastClickTime = 0L

    override fun onClick(view: View) {
        if (SystemClock.elapsedRealtime() - lastClickTime < 1000) {
            return
        }
        lastClickTime = SystemClock.elapsedRealtime()

        block()
    }
}

fun View.setOnSingleClickListener(block: () -> Unit) {
    setOnClickListener(OnSingleClickListener(block))
}

使用方法:

button.setOnSingleClickListener { ... }

或者添加一个参数以控制节流

class OnClickListenerThrottled(private val block: () -> Unit, private val wait: Long) : View.OnClickListener {

    private var lastClickTime = 0L

    override fun onClick(view: View) {
        if (SystemClock.elapsedRealtime() - lastClickTime < wait) {
            return
        }
        lastClickTime = SystemClock.elapsedRealtime()

        block()
    }
}

/**
 * A throttled click listener that only invokes [block] at most once per every [wait] milliseconds.
 */
fun View.setOnClickListenerThrottled(wait: Long = 1000L, block: () -> Unit) {
    setOnClickListener(OnClickListenerThrottled(block, wait))
}

用途:

button.setOnClickListenerThrottled(2000L) { /** some action */}
or
button.setOnClickListenerThrottled { /** some action */}

这样,点击动画可以播放多次,而实际上只会触发一次点击。使用“disable”方法更方便,以防止多个点击动画播放。 - Stan

18

我也遇到了类似的问题,我正在展示一些日期选择器和时间选择器,有时会被点击两次。我用以下方法解决了这个问题:

long TIME = 1 * 1000;
@Override
public void onClick(final View v) {
v.setEnabled(false);
    
    new Handler().postDelayed(new Runnable() {

        @Override
        public void run() {
            v.setEnabled(true);
        }
    }, TIME);
}

您可以根据自己的需求更改时间。


这是真正的解决方案,因为它在时间到期后自动重新激活按钮。其他解决方案会永久阻止按钮操作,如果你快速点击两次。这个解决方案修复了问题!谢谢。 - Néstor

17

我知道这是个老问题,但我分享了我发现的最佳解决方案来解决这个常见问题。

        btnSomeButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            // Prevent Two Click
            Utils.preventTwoClick(view);
            // Do magic
        }
    });

而在另一个文件中,比如Utils.java

    /**
 * Método para prevenir doble click en un elemento
 * @param view
 */
public static void preventTwoClick(final View view){
    view.setEnabled(false);
    view.postDelayed(
        ()-> view.setEnabled(true),
        500
    );
}

9

setEnabled(false)对我来说运作得很好。

我的想法是一开始我会写上{ setEnabled(true); },并且在第一次点击按钮时将其设置为false


8

我的解决方案是尝试使用一个布尔变量:boolean

public class Blocker {
    private static final int DEFAULT_BLOCK_TIME = 1000;
    private boolean mIsBlockClick;

    /**
     * Block any event occurs in 1000 millisecond to prevent spam action
     * @return false if not in block state, otherwise return true.
     */
    public boolean block(int blockInMillis) {
        if (!mIsBlockClick) {
            mIsBlockClick = true;
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    mIsBlockClick = false;
                }
            }, blockInMillis);
            return false;
        }
        return true;
    }

    public boolean block() {
        return block(DEFAULT_BLOCK_TIME);
    }
}

以下是使用方法:

view.setOnClickListener(new View.OnClickListener() {
            private Blocker mBlocker = new Blocker();

            @Override
            public void onClick(View v) {
                if (!mBlocker.block(block-Time-In-Millis)) {
                    // do your action   
                }
            }
        });

更新: 使用视图扩展的Kotlin解决方案

fun View.safeClick(listener: View.OnClickListener, blockInMillis: Long = 500) {
    var lastClickTime: Long = 0
    this.setOnClickListener {
        if (SystemClock.elapsedRealtime() - lastClickTime < blockInMillis) return@setOnClickListener
        lastClickTime = SystemClock.elapsedRealtime()
        listener.onClick(this)
    }
}

太棒了。Kotlin扩展起作用了。谢谢。 - groff07

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接