快速点击RecyclerView会打开多个Fragment

22

我在我的RecyclerView的ViewHolder中实现了onClick监听器。

但是当我进行非常快速的双击或鼠标点击时,它会执行任务(在这种情况下打开一个单独的片段)两次或三次。

以下是我的代码:

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
    TextView tvTitle, tvDescription;

    public ViewHolder(View itemView) {
        super(itemView);
        itemView.setClickable(true);
        itemView.setOnClickListener(this);

        tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
        tvDescription = (TextView) itemView.findViewById(R.id.tv_description);
    }

    @Override
    public void onClick(View v) {
        mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open FRAGMENT_VIEW
    }
}

有没有任何想法来防止这种行为?

14个回答

31

你可以像这样进行修改。

public class ViewHolder extends RecyclerView.ViewHolder implements
        View.OnClickListener {
    TextView tvTitle, tvDescription;
    private long mLastClickTime = System.currentTimeMillis();
    private static final long CLICK_TIME_INTERVAL = 300;

    public ViewHolder(View itemView) {
        super(itemView);
        itemView.setClickable(true);
        itemView.setOnClickListener(this);

        tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
        tvDescription = (TextView) itemView
                .findViewById(R.id.tv_description);
    }

    @Override
    public void onClick(View v) {
        long now = System.currentTimeMillis();
        if (now - mLastClickTime < CLICK_TIME_INTERVAL) {
            return;
        }
        mLastClickTime = now;
        mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open
                                                                // FRAGMENT_VIEW
    }
}

这个问题让我纠结了几分钟,谢谢,兄弟,它起作用了! - Farwa
18
答案不错,但只解决了同一项被触碰的情况。我的意思是,如果用户先触摸item1,然后立即触摸item2,则会打开两个片段。至少对我来说是这样的。 - Roger
@Roger,你采用了什么解决方案?我在考虑使用rxJava中的debounce或throttleFirst,但这只能解决特定视图的问题。 - Abubakar
@Roger 将点击逻辑移入适配器以处理列表中每个项目的点击:将接口传递给viewHolder并在每个点击监听器上调用它。在适配器中实现接口并使用CLICK_TIME_INTERVAL防止多次点击。 - Nicolas M.

26

在这里最直接的方法是在您的RecyclerView中使用setMotionEventSplittingEnabled(false)

默认情况下,RecyclerView中将其设置为true,允许处理多个触摸。

当设置为false时,此ViewGroup方法将防止RecyclerView子项接收多个点击事件,仅处理第一个。

了解更多信息,请单击此处


简洁明了的解决方案。 - Meet Vora
8
这并不是真的,内部文档证实了这一点:
  • @param split <code>true</code> 可以使 MotionEvent 被分割并发送到多个子视图中。
    • false 只允许一个子视图成为此 ViewGroup 接收的任何 MotionEvent 的目标。
简单来说,设置此参数仅会阻止不同子视图处理多个触摸事件,但是一个子视图仍然可以接收到很多触摸事件。(更简单地说,点击 a -> b 不可能,但是 a -> a 的双击仍然是可能的。)
- Trevor Hart
太棒了,简短而有力! - dave o grady
setMotionEventSplittingEnabled(false) 运行得很好 :) - Rohit Patil
@joao2fast4u,但它只防止了对多个条目的回调,而不是对同一项的多次点击...我认为可能还有其他防抖的方面。 - Vikas Pandey

7

这是非常令人烦恼的行为。在我的工作中,我必须使用一个额外的标记来防止这种情况发生。

public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
TextView tvTitle, tvDescription;
private boolean clicked;

public ViewHolder(View itemView) {
    super(itemView);
    itemView.setClickable(true);
    itemView.setOnClickListener(this);

    tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
    tvDescription = (TextView) itemView.findViewById(R.id.tv_description);
}

@Override
public void onClick(View v) {
    if(clicked){
        return;
    }
    clicked = true;
    v.postDelay(new Runnable(){
          @Override
          public void run(View v){
              clicked = false;
          }
    },500);
    mListener.onClick(FRAGMENT_VIEW, getAdapterPosition()); // open FRAGMENT_VIEW
}
}

1
谢谢,你的概念是正确的并且它有效,但是一定有一些适当的处理方式,我还不知道它,无论如何再次感谢。 - Shifatul
使用 new Handler().postDelayed(new Runnable() {...} 替代 v.postDelay(new Runnable(){...}。 - Shifatul
如果(clicked){ 返回; } clicked = true; v.postDelayed(new Runnable(){ @Override public void run() { clicked = false; } },500); - Shyam Sunder
我认为这不是一个好的方法,较慢的设备可能无法在半秒钟内完成单击事件,具体取决于单击事件中发生了什么,这将导致相同的问题。 - Trevor Hart

6
如果您正在使用Kotlin,可以基于Money的答案进行操作。
class CodeThrottle {
    companion object {
        const val MIN_INTERVAL = 300
    }
    private var lastEventTime = System.currentTimeMillis()

    fun throttle(code: () -> Unit) {
        val eventTime = System.currentTimeMillis()
        if (eventTime - lastEventTime > MIN_INTERVAL) {
            lastEventTime = eventTime
            code()
        }
    }
}

在你的视图持有者中创建这个对象。
    private val codeThrottle = CodeThrottle()

在您的绑定中,请按照以下步骤执行。
name.setOnClickListener { codeThrottle.throttle { listener.onCustomerClicked(customer, false) } }

将您需要调用的任何代码放置在

所在的位置。
listener.onCustomerClicked(customer, false) 

3
请将以下属性添加到您的主题中:
<item name="android:splitMotionEvents">false</item>
<item name="android:windowEnableSplitTouch">false</item>

这将防止同时进行多次点击。

1
在适配器中创建一个布尔变量。
boolean canStart = true;

将OnClickListener设置为如下:
ViewHolder.dataText.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (canStart) {
            canStart = false; // do canStart false 
            // Whatever you want to do and not have run twice due to double tap
        }
    }
}
  • 在适配器类中添加 setCanStart 方法:
public void setCanStart(boolean can){
    canStart = can;
}
  • 最后,在片段或活动中(在适配器分配到recyclerview的位置),添加此onResume()方法。
@Override
    public void onResume() {
        super.onResume();
        mAdapter.setCanStart(true);
    }

希望它有所帮助 :)

这样做行不通,因为dispatchTouchEvent会排队下一个点击处理程序,并在大多数系统有机会将其单击处理程序中的布尔值设置为true之前运行它,因此在许多甚至大多数情况下,这不会是可行的选项。 - Trevor Hart

1

RecyclerView上的快速点击可能会导致两种情况-

  1. RecyclerView中的单个项目被多次点击。 这可能会导致目标片段被多次创建,从而使单个片段堆叠多次,破坏用户的流畅体验。

  2. 同时点击RecyclerView的多个项目。 这可能会导致应用程序的不良行为。(再次打开多个片段。)

为了获得良好的应用程序运行体验,应处理这两种情况。 为了防止第一种情况,您可以使用逻辑,在一定时间间隔内,如果该项被多次点击,则不应创建新片段。 以下是代码-

    class ViewHolder extends RecyclerView.ViewHolder{
     //Suppose your item is a CardView
     private CardView cardView;
      private static final long TIME_INTERVAL_GAP=500;
      private long lastTimeClicked=System.currentTimeMillis();
      public ViewHolder(@NonNull View itemView)
        {
        cardView=itemView.findViewById(R.id.card_view);
        
          cardView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            long now=System.currentTimeMillis();
            //check if cardView is clicked again within the time interval gap
            if(now-lastTimeClicked<TIME_INTERVAL_GAP)
                return;       //no action to perform if it is within the interval gap.
            mLastClickTime=now;
            //... Here your code to open a new fragment  
             }
         });
         
         }

     
      }

第二种情况的解决方案-RecyclerView有一个方法setMotionEventSplittingEnabled(boolean split)

文档说-

启用或禁用在触摸事件分派期间将MotionEvents拆分到多个子项中。对于目标版本为HONEYCOMB或更高版本的应用程序,默认情况下启用此行为。

当启用此选项时,MotionEvents可能会被拆分并分派到不同的子视图中,具体取决于每个指针最初进入的位置。这允许用户进行诸如独立滚动两个内容窗格、按键和在不同内容片段上执行独立手势等用户交互。将Split设置为true以允许MotionEvents被拆分并分派到多个子视图中。将其设置为false,只允许一个子视图成为目标。

因此,在您的代码中只需添加一行代码-

     recyclerView.setMotionEventSplittingEnabled(false);

这些肯定能解决由于快速点击RecyclerView而导致的问题,并防止您的应用程序不必要地堆叠相同的片段。


1
我知道这个回答有点晚了,而且已经有了答案,但我发现我的情况类似的问题是由第三方库Material Ripple Layout引起的。默认情况下,它启用了一个延迟调用onClick并允许多个请求被发送到onClick,因此当动画完成时,所有这些点击将一次性注册,并打开多个对话框。
在我的情况下,这个设置取消了延迟并解决了问题。
app:mrl_rippleDelayClick="false"

0

我重新利用了Butterknife中的DebouncingOnClickListener,在防止多个视图上的点击的同时,将其重新设计为在指定时间内去抖动点击。

要使用它,请扩展它并实现doOnClick方法。

DebouncingOnClickListener.kt

import android.view.View

/**
 * A [click listener][View.OnClickListener] that debounces multiple clicks posted in the
 * same frame and within a time frame. A click on one view disables all view for that frame and time
 * span.
 */
abstract class DebouncingOnClickListener : View.OnClickListener {

    final override fun onClick(v: View) {
        if (enabled && debounced) {
            enabled = false
            lastClickTime = System.currentTimeMillis()
            v.post(ENABLE_AGAIN)
            doClick(v)
        }
    }

    abstract fun doClick(v: View)

    companion object {
        private const val DEBOUNCE_TIME_MS: Long = 1000

        private var lastClickTime = 0L // initially zero so first click isn't debounced

        internal var enabled = true
        internal val debounced: Boolean
            get() = System.currentTimeMillis() - lastClickTime > DEBOUNCE_TIME_MS

        private val ENABLE_AGAIN = { enabled = true }
    }
}

0
为了防止多次点击,您可以使用AtomicBoolean作为标志来跟踪是否正在进行点击。在执行单击操作之前,请检查标志,并在已经有一个单击正在进行时提前返回。
否则,将标志设置为true,执行单击操作,并在操作完成时将标志重置为false
以下是使用函数属性作为单击监听器委托和修改后的onViewAttachedToWindow方法来防止对视图项进行多次点击的通用解决方案:
class YourAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // Click Listener Delegate
    var onItemClickListener: ((item: Any, itemView: View) -> Unit)? = null

    private val clickInProgress = AtomicBoolean(false)

    override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
        super.onViewAttachedToWindow(holder)
        holder.itemView.setOnClickListener { view ->
            // Check if click is in progress
            if (clickInProgress.compareAndSet(false, true)) {
                val position = holder.adapterPosition
                if (position != RecyclerView.NO_POSITION) {
                    val item = getItem(position) // Replace with your method to get the item based on the position
                    onItemClickListener?.invoke(item, view)
                    clickInProgress.set(false) // Reset the clickInProgress state after the action is complete
                } else {
                    clickInProgress.set(false)
                }
            }
        }
    }

    override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
        super.onViewDetachedFromWindow(holder)
        holder.itemView.setOnClickListener(null)
    }

    // Implement the rest of your adapter methods here
}

在这个解决方案中,我们创建了一个onItemClickListener,它以itemview作为参数。我们使用适配器位置使用getItem()函数获取相应的项目。您应该使用适合您的适配器的适当方法替换getItem()调用,以根据位置获取项目。
要使用此解决方案,只需使用处理单击事件的lambda函数设置适配器中的onItemClickListener属性即可。
yourAdapter.onItemClickListener = { item, itemView ->
    // Handle the click event here
}

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