18

I have implemented onClick listener to my ViewHolder for my RecyclerView

But when I perform very fast double taps or mouse clicks, it performs the task (opens a seperate fragment in this case) twice or three times.

here is my code

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

Any ideas on how to prevent such behaviour?

Shifatul
  • 1,891
  • 3
  • 23
  • 34

11 Answers11

27

You can modify it like this.

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
    }
}
Money
  • 306
  • 2
  • 3
  • This issue was burning my head for last couple of minutes. Thanks man it worked! – Farwa Feb 01 '18 at 11:56
  • 18
    The answer is good but it only solves the issue for the case on which the same item is touched. What I mean is that if the user touches item1 and immediately touches item2 then two fragments will open. At least, this is happening to me. – Roger Jun 15 '18 at 21:52
  • @Roger What solution did you implement? I am thinking to use debounce or throttleFirst from rxJava but that solves the problem on specific views. – Abubakar Feb 20 '20 at 14:09
  • @Roger move the click logic into the adapter to handle the click on every item of your list: Pass an interface to the viewHolder and call it on every click listener. In the adapter implement interface and prevent multiple click with the CLICK_TIME_INTERVAL – Nicolas M. Mar 11 '20 at 15:45
21

The most straightforward approach here would be using setMotionEventSplittingEnabled(false) in your RecyclerView.

By default, this is set to true in RecyclerView, allowing multiple touches to be processed.

When set to false, this ViewGroup method prevents the RecyclerView children to receive the multiple clicks, only processing the first one.

See more about this here.

joao2fast4u
  • 6,574
  • 5
  • 25
  • 41
  • short and sweet solution. – Meet Vora Jul 10 '17 at 07:45
  • 8
    This isn't true, and the internal docs confirm it: * @param split true to allow MotionEvents to be split and dispatched to multiple * child views. false to only allow one child view to be the target of * any MotionEvent received by this ViewGroup. in simple terms, setting this only blocks multiple touch events from being processed by different children, but a child can still receive many touch events. (or even more simply put, click a -> b not possible, but a -> a double tap is still possible) – Trevor Hart Feb 12 '19 at 19:40
  • awesome short and sweet – dave o grady Apr 05 '20 at 17:52
  • setMotionEventSplittingEnabled(false) Wokrs like charm :) – Rohit Patil Jan 20 '21 at 13:06
7

This is a very annoying behavior. I have to use an extra flag to prevent this in my work.

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
}
}
songchenwen
  • 1,322
  • 9
  • 14
  • **thanks**, ur concept is right and it works, but there must be some proper way of handling it, i just don't know it yet, thank again anyway. – Shifatul Aug 07 '15 at 03:17
  • use --> new Handler().postDelayed(new Runnable() {...} instead of v.postDelay(new Runnable(){...} – Shifatul Aug 07 '15 at 08:17
  • Small change if(clicked){ return; } clicked = true; v.postDelayed(new Runnable(){ @Override public void run() { clicked = false; } },500); – Shyam Sunder Jun 26 '18 at 19:53
  • I don't think this is a good approach, slower devices may not be able to finish the click event in a half second depending on what happens in the click event, which is going to result in the same issue. – Trevor Hart Feb 12 '19 at 19:42
4

If you are using Kotlin you can go with this based on Money's answer

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

Create this object in your view holder

    private val codeThrottle = CodeThrottle()

Then do the following in your bind

name.setOnClickListener { codeThrottle.throttle { listener.onCustomerClicked(customer, false) } }

Putting whatever code you need called in place of

listener.onCustomerClicked(customer, false) 
Kevin
  • 1,102
  • 11
  • 15
2

Add below attributes in your theme

<item name="android:splitMotionEvents">false</item>
<item name="android:windowEnableSplitTouch">false</item>

This will prevent multiple tap at same time.

Alex Chengalan
  • 7,361
  • 3
  • 36
  • 54
1
  • Create a boolean variable in Adapter
boolean canStart = true;
  • Make OnClickListener like
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
        }
    }
}
  • Add setCanStart method in Adapter class:
public void setCanStart(boolean can){
    canStart = can;
}
  • At last in the fragment's or Activity's ( Where the adapter is assigned to the recyclerview ) add this onResume()
@Override
    public void onResume() {
        super.onResume();
        mAdapter.setCanStart(true);
    }

Hope it will help :)

Boken
  • 3,207
  • 9
  • 25
  • 31
Shujat Munawar
  • 1,289
  • 16
  • 22
  • This won't work because the dispatchTouchEvent queues up the next click handler and runs it well before most systems get the chance to set the boolean in their click handlers to true, so this isn't going to be a viable option in many or even most cases. – Trevor Hart Feb 12 '19 at 19:32
0

I repurposed the DebouncingOnClickListener from Butterknife to debounce clicks within a specified time, in addition to preventing clicks on multiple views.

To use, extend it and implement 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 }
    }
}
Ryan R
  • 7,902
  • 14
  • 74
  • 107
0

You can make class implementing View.OnClickListener

public class DoubleClickHelper implements View.OnClickListener {

    private long mLastClickTime = System.currentTimeMillis();
    private static final long CLICK_TIME_INTERVAL = 300;
    private Callback callback;

    public DoubleClickHelper(Callback callback) {
        this.callback = callback;
    }

    @Override
    public void onClick(View v) {
        long now = System.currentTimeMillis();
        if (now - mLastClickTime < CLICK_TIME_INTERVAL) {
            return;
        }
        mLastClickTime = now;
        callback.handleClick();
    }

    public interface Callback {
        void handleClick();
    }
}

And than use it like:

ivProduct.setOnClickListener(new DoubleClickHelper(() -> listener.onProductInfoClick(wItem)));
Ilia Grabko
  • 931
  • 1
  • 7
  • 13
0

I know this is late and the answer has already been given, but I found that this similar issue for my case was due to a third party library Material Ripple Layout. By default it enables a delay call to onClick and allows for multiple request to be made to onClick so when the animation finishes all those clicks get registered at once and opens multiple dialogs.

this setting cancels the delay and fixed the problem for me in my case.

app:mrl_rippleDelayClick="false"
xZ4FiRx
  • 75
  • 1
  • 11
0

Too late but it can work for other people:

recyclerAdapter.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            int id = ....
            if(id == 1){
                view.setClickable(false); //add this
                Intent a = new Intent...
                startActivity(a);

            }else if(id == 2){
                view.setClickable(false);
                Intent b = ...
                startActivity(b);
            }
        }
    });

Fragment - onResume()

@Override
public void onResume() {
    super.onResume();
    Objects.requireNonNull(getActivity()).invalidateOptionsMenu();
    recyclerView.setAdapter(recyclerAdapter); //add this
}

It works for me, i don't know if it's correct.

0

Fast taps (clicks) on RecyclerView can cause two situations-

  1. A single item of RecyclerView clicked multiple times. This can cause the destination fragment to be created multiple times thereby making a single fragment stack up multiple times, disrupting the user's smooth experience.

  2. Multiple items of the RecyclerView being clicked at a time. This can cause the undesired behavior of the app. (Opening multiple fragments again.)

Both situations should be handled for a proper app running experience. To prevent 1st situation you can use the logic that within a definite interval of time if the item is clicked more than once then it should not go for creating a new fragment. Here is the code below-

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

     
      }

Solution for 2nd scenario- RecyclerView has a method setMotionEventSplittingEnabled(boolean split)

The documentation says-

Enable or disable the splitting of MotionEvents to multiple children during touch event dispatch. This behavior is enabled by default for applications that target an SDK version of HONEYCOMB or newer.

When this option is enabled MotionEvents may be split and dispatched to different child views depending on where each pointer initially went down. This allows for user interactions such as scrolling two panes of content independently, chording of buttons and performing independent gestures on different pieces of content. Split set to true to allow MotionEvents to be split and dispatched to multiple child views. and set it to false to only allow one child view to be the target of.

So in your code just add a line of code-

     recyclerView.setMotionEventSplittingEnabled(false);

These will surely solve the problem caused due to fast taps on the RecyclerView and prevent your app to stack up the same fragment unnecessarily.