21

I have a RecyclerView with 2 items that don't fill the whole screen. How can I detect that the user clicked on the empty part of the RecyclerView (meaning clicked directly on the RecyclerView and not one of its items)?

Omar
  • 7,835
  • 14
  • 62
  • 108

7 Answers7

23

As mentioned in the comment

mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {

  @Override
  public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
    if (motionEvent.getAction() != MotionEvent.ACTION_UP) {
        return false;
    }
    View child = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
    if (child != null) {
      // tapped on child
      return false;
    } else {
      // Tap occured outside all child-views.
      // do something
      return true;
    }
  }

  @Override
  public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
  }
});
Angel Kjos
  • 1,951
  • 1
  • 12
  • 12
  • Should also check MotionEvent.getAction() == ACTION_UP, otherwise, you get 2 click events. – Greg Ennis Jan 06 '17 at 14:32
  • 1
    This should be the accepted answer. Also it should probably be `return false;` rather than `return;` under the `MotionEvent.ACTION_UP` portion. – Zack Morris Dec 20 '17 at 04:31
  • 1
    This works with one problem: One some devices, clicking on a view makes a sound. In this case it does not. This can be solved by calling `mRecyclerView.playSoundEffect(SoundEffectConstants.CLICK);` just before returning `true`. – Minas Mina Apr 10 '19 at 20:39
12

You can subclass RecyclerView and override the dispatchTouchEvent() method to accomplish this. Using the findChildViewUnder() method, we can determine if a touch event occurs outside of the child Views, and use an interface to notify a listener if it is. In the following example, the OnNoChildClickListener interface provides that functionality.

public class TouchyRecyclerView extends RecyclerView
{
    // Depending on how you're creating this View,
    // you might need to specify additional constructors.
    public TouchyRecyclerView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }

    private OnNoChildClickListener listener;
    public interface OnNoChildClickListener
    {
        public void onNoChildClick();
    }

    public void setOnNoChildClickListener(OnNoChildClickListener listener)
    {
        this.listener = listener;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
        // The findChildViewUnder() method returns null if the touch event
        // occurs outside of a child View.
        // Change the MotionEvent action as needed. Here we use ACTION_DOWN
        // as a simple, naive indication of a click.
        if (event.getAction() == MotionEvent.ACTION_DOWN
            && findChildViewUnder(event.getX(), event.getY()) == null)
        {
            if (listener != null)
            {
                listener.onNoChildClick();
            }
        }
        return super.dispatchTouchEvent(event);
    }
}

NB: This is adapted for RecyclerView from my answer here concerning GridView.

Community
  • 1
  • 1
Mike M.
  • 38,532
  • 8
  • 99
  • 95
  • 1
    Thank you very much! I ended up using recyclerView.addOnItemTouchListener with findChildViewUnder(..). – Omar Dec 30 '14 at 13:04
  • @Omar Ah! Very good! I'm still familiarizing myself with RecyclerView, and I wasn't yet aware of that interface. Thanks for the info! You should create an answer for that. You'll get an upvote from me. – Mike M. Dec 30 '14 at 13:08
4

@driss-bounouar's answer is almost right although this will prevent the user from scrolling the recycler view as any down event will cause your action to happen. With a slight modification where we record the down event and then check on the up event if the coordinates have not changed much, then fire the event.

private MotionEvent lastRecyclerViewDownTouchEvent;
myRecyclerView.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (event.getAction() == MotionEvent.ACTION_DOWN && myRecyclerView.findChildViewUnder(event.getX(), event.getY()) == null) {
                            lastRecyclerViewDownTouchEvent = event;
                        } else if (event.getAction() == MotionEvent.ACTION_UP && myRecyclerView.findChildViewUnder(event.getX(), event.getY()) == null
                                && lastRecyclerViewDownTouchEvent != null) {
                            // Check to see if it was a tap or a swipe
                            float xDelta = Math.abs(lastRecyclerViewDownTouchEvent.getX() - event.getX());
                            float yDelta = Math.abs(lastRecyclerViewDownTouchEvent.getY() - event.getY());
                            if (xDelta < 30 && yDelta < 30) {
                                // Do action
                            }
                            lastRecyclerViewDownTouchEvent = null;
                        }
                        return false;
                    }
                });
odiggity
  • 1,496
  • 16
  • 29
1

You just need to set a TouchListener on the RecyclerView like shown above :

categoryTable.setAdapter(new CatgoriesAdapter(categories.getWrappedList()));
    categoryTable.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (event.getAction() == MotionEvent.ACTION_DOWN
                    && categoryTable.findChildViewUnder(event.getX(), event.getY()) == null)
            {
                // Touch outside items here, you do whatever you want  
                HideCategoryMenu();
            }
            return false;
        }
    });
Driss Bounouar
  • 3,182
  • 2
  • 32
  • 49
1

@Angel Kjoseski answer would look like this in Kotlin:

yourRecyclerView.addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
                override fun onInterceptTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent): Boolean {
                    return when {
                        motionEvent.action != MotionEvent.ACTION_UP || recyclerView.findChildViewUnder(motionEvent.x, motionEvent.y) != null -> false
                        else -> {
                            // do something here
                            true
                        }
                    }
                }

                override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {}
                override fun onTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent) {}
            })
Rvb84
  • 675
  • 1
  • 6
  • 14
1

This works for me, I use it instead RecyclerView:

public class ClickeableRecyclerView extends RecyclerView
{
    private static final int CLICK_DURATION = 800;
    private CountDownTimer clickCountDown;
    private final Object sincClick = new Object();
    private boolean shortClick = true;

    public ClickeableRecyclerView(@NonNull Context context)
    {
        super(context);
        init();
    }

    public ClickeableRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs)
    {
        super(context, attrs);
        init();
    }

    public ClickeableRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init()
    {
        clickCountDown = new CountDownTimer(CLICK_DURATION, 100)
        {
            public void onTick(long millisUntilFinished)
            {
                synchronized(sincClick)
                {
                    shortClick = true;
                }
            }
            public void onFinish()
            {
                synchronized(sincClick)
                {
                    shortClick = false;
                }
            }
        };
    }

    @Override
    public boolean onTouchEvent(MotionEvent e)
    {
        if(findChildViewUnder(e.getX(), e.getY()) == null)
        {
            if(e.getAction() == MotionEvent.ACTION_DOWN)
            {
                clickCountDown.start();
            }
            else if(e.getAction() == MotionEvent.ACTION_UP)
            {
                synchronized(sincClick)
                {
                    if(shortClick)
                    {
                        performClick();
                        clickCountDown.cancel();
                    }
                }
            }
        }
        else
        {
            clickCountDown.cancel();
        }
        return super.onTouchEvent(e);
    }

    @Override
    public boolean performClick()
    {
        return super.performClick();
    }
}

In the layout:

<yourpackage.ClickeableRecyclerView
    android:id="@+id/recyclerViewItems"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:background="@color/white"
    android:visibility="visible"
    app:layout_constraintBottom_toTopOf="@+id/viewTotal"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/buttonShowItems"
    tools:visibility="visible" />

In the activity:

private ClickeableRecyclerView recyclerView;

recyclerView = findViewById(R.id.recyclerView);

recyclerView.setOnClickListener(new View.OnClickListener()
{
    @Override
    public void onClick(View v)
    {
        // Do something when click no items
    }
});

I hope it helps.

EverCpp
  • 53
  • 5
0
shortcutDeviceRecly.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {

                if(event.getAction() == MotionEvent.ACTION_UP){
                    roomClickEvent(true, v);
                }

                return false;
            }
        });
Ninad Kambli
  • 745
  • 7
  • 12