Wednesday, July 12, 2017

Topmost View swallows touch events because overlapping

Leave a Comment

So here's my layout structure:

- RelativeLayout (full screen)   - FrameLayout (full screen)   - Button (in the middle of the screen)   - RecyclerView (align parent bottom but with very big padding top to      capture the scrolling also in the top of the screen, so it's basically acting like a full screen `RecyclerView`) 

So yes, the views are overlapping each other.

As of the reason the RecyclerView is the topmost view, it captures all the touch events on screen, and swallows them, preventing any touches to "go through it" to the underlying views below it. (Note: by underlying views, I don't mean to the RecyclerView's children but the other rootview's children)

I've read tons of stackoverflow posts regarding propagating touch events and preventing swallowing touch events, etc, and even though it seems pretty straightforward task to accomplish, I couldn't achieve the following effect:

I want my RecyclerView to capture the touch event, so it will scroll or whatever. but I want the rootView to think as that the RecyclerView didn't capture the event, and continue pass it to its other children (rootview' s children).

Here's What I've tried to do:

1. Overriding dispatchTouchEvent of the RecyclerView to do its logic and return false to act as it didn't dispatch its touch event so the rootview will keep iterating for touch through its child views.

@Override public boolean dispatchTouchEvent(MotionEvent ev) {     super.dispatchTouchEvent(ev);     return false; } 

What happened:

The RecyclerView still functions but still swallows all touch events (just the same as before)

2. Overriding onTouchEvent of the RecyclerView to do its logic and return false. (Note: I know it doesn't seem as it would be the solution but I tried)

@Override public boolean onTouchEvent(MotionEvent e) {     super.onTouchEvent(e);     return false; } 

What happened:

Same result as in #1

I've made some more tweaks with the same idea, but they didn't work as well, so I'm kinda clueless right now and would love for help of you fellows!

3 Answers

Answers 1

I have two suggestions for you:
The first is simpler but assumes that you want to click on the Button and it can appear above the RecyclerView, if this is the case you can simply change the layout's order:

- RelativeLayout (full screen)   - FrameLayout (full screen)   - RecyclerView   - Button (in the middle of the screen) 

If you need something more general then it can be done like this:

recyclerView.setOnTouchListener(new View.OnTouchListener() {             @Override             public boolean onTouch(View v, MotionEvent event) {                 int [] pos = new int[2];                 recyclerView.getLocationOnScreen(pos);                 Rect rect = new Rect();                 button.getHitRect(rect);                 if (rect.contains((int) event.getX() + pos[0], (int) event.getY() + pos[1])) {                     button.onTouchEvent(event);                 }                 return false;             }         }); 

You will need to handle all your views separately, which is not as nice, but it works in my test.

Note: if you have added addOnItemTouchListenerto your RecyclerView you will need to add the said functionality to onInterceptTouchEvent (or to both places, depends on your items and RecyclerView).

Answers 2

I'm not sure how efficient/valid/nice solution is this, but this is what comes to my mind: what if you listen for touch events in the root view (RelativeLayout in your case), and dispatch the touch event to all the children of that layout except for the RecyclerView?

public class MyRelativeLayout extends RelativeLayout {     ...      @Override     public boolean onInterceptTouchEvent(MotionEvent ev) {         final boolean intercept = super.onInterceptTouchEvent(ev);          // getChildCount() - 1, because `RecyclerView` is the last child         for (int i = 0, size = getChildCount() - 1; i < size; i++) {             View v = getChildAt(i);             v.dispatchTouchEvent(MotionEvent.obtain(ev));         }          return intercept;     } } 

This seems like it should work.

Answers 3

I don't quite understand your problem, however if the goal is to not allow RecyclerView to consume any and all touch events, then simply set android:elevation="1dp"for the FrameLayout and Override dispatchTouchEvent() like so:

@Override public boolean dispatchTouchEvent(MotionEvent event) {     Logger.log("CustomFrameLayout : dispatchTouchEvent");     boolean result = super.dispatchTouchEvent(event);      ((View) getParent()).findViewById(R.id.recyclerView).dispatchTouchEvent(event);      return result;                 // This part can be changed according to requirement. } 

This may or may not have some undesired side effects, since I just came up with it on my own, however as much as I have checked it does not.

Cases I've tested:

  1. Scrolling in RecyclerView : Works.
  2. Clicks in RecyclerView Items : Works.
  3. Clicks in Children of FrameLayout : Works.

UPDATE :

Why is there any need for elevation for the FrameLayout?

android:elevation provides a means for position of z-axis of any layout. As to why there is a need to do so, to answer that I'll have to delve deeper and bore you with lengthy explanation (There is already loads of stuff written on it) suffice it to say that MotionEvents trickle down Views following some basic rules: From root to its children, The child to first get the event first (to check if the event belongs to it) trickles down on the basis of their position in the x-y axis and then on their order in z-axis (Top one gets the event first).


So In your case when a MotionEvent is detected it is passed to the RelativeLayout (RootView), which has 3 children, Button will be filtered because of the position. Now you have 2 Views a FrameLayout and RecyclerView. A RelativeLayout's bottom-most child comes on top of the z-axis, which means RecyclerView event will be dispatched to it first (which will naturally consume the event). If however a case arose in which the event was not consumed (your first attempt), the event will be passed on to the next child View until there are no more child Views left at which point the parent (RelativeLayout) will assume the event belongs to itself and call its own onInterceptTouchEvent(). During this process if a View has consumed the event all the subsequent calls will be passed to it directly (The reason you can only make one of the two either FrameLayout or RecyclerView work).

All of the above explanation means by the rulebook only one View will ever consume the event.

What I Did

I simply elevated the FrameLayout so it would be the official receiver of the event. Overrode the dispatchTouchEvent() and sent the same event to its neighboring RecyclerView too, which thinks the event has been brought to it by regular mode.

Now after all that explanation

The comment // This part can be changed according to requirement.

A better and possibly error-less method would be

boolean result2 = ((View) getParent()).findViewById(R.id.recyclerView).dispatchTouchEvent(event);  return result || result2; 

So if any of the Views have consumed the event the root will think that it has been consumed too.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment