Controlling touch event propagation for overlapping views

I have some problem trying to control the distribution of touch events in my RecycleView. Thus, I have a RecyclerView populating a set of CardViews that mimic a stack of cards (so they overlap each other with a certain movement, although they have a different height value). My current problem is that there is a button on each map, and since the relative displacement of the map is less than the height of the button, this causes the buttons to overlap, and whenever a touch event is dispatched, it starts to propagate from the lower hierarchy of views (from children with the highest number of children).

In accordance with the articles I read ( this , this , as well as this video ). The touch extends depends on the order of the views in the parent view, so the touch will first be passed to the child using the highest index, while I want the touch event to be handled only by the top view and RecyclerView touches (it should also handle drag and drop gestures). I currently use a backup of cards that are aware of their position in the hierarchy of parent views to prevent child abuse, but this is a really ugly way to do this. My assumption is that I should override the RecaViewView dispatchTouchEvent method and correctly send the touch event only to the very top child. However, when I tried this way to do this (which is also pretty awkward):

@Override public boolean dispatchTouchEvent(MotionEvent ev) { View topChild = getChildAt(0); for (View touchable : topChild.getTouchables()) { int[] location = new int[2]; touchable.getLocationOnScreen(location); RectF touchableRect = new RectF(location[0], location[1], location[0] + touchable.getWidth(), location[1] + touchable.getHeight()); if (touchableRect.contains(ev.getRawX(), ev.getRawY())) { return touchable.dispatchTouchEvent(ev); } } return onTouchEvent(ev); } 

Only the DOWN button was delivered to the button inside the card (without triggering a click). I would appreciate any advice on how to reverse the distribution of sensory events or delegate a touch event to a specific view. Thank you so much in advance.

EDIT: This is a screenshot of what an example map stack looks like Screen

Adapter code example:

 public class TestAdapter extends RecyclerView.Adapter { List<Integer> items; public TestAdapter(List<Integer> items) { this.items = items; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) .inflate(R.layout.test_layout, parent, false); return new TestHolder(v); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { TestHolder currentHolder = (TestHolder) holder; currentHolder.number.setText(Integer.toString(position)); currentHolder.tv.setTag(position); currentHolder.tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int pos = (int) v.getTag(); Toast.makeText(v.getContext(), Integer.toString(pos) + "clicked", Toast.LENGTH_SHORT).show(); } }); } @Override public int getItemCount() { return items.size(); } private class TestHolder extends RecyclerView.ViewHolder { private TextView tv; private TextView number; public TestHolder(View itemView) { super(itemView); tv = (TextView) itemView.findViewById(R.id.click); number = (TextView) itemView.findViewById(R.id.number); } } } 

and a sample map layout:

 <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_margin="16dp" android:textSize="24sp" android:id="@+id/number"/> <TextView android:layout_width="wrap_content" android:layout_height="64dp" android:gravity="center" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_margin="16dp" android:textSize="24sp" android:text="CLICK ME" android:id="@+id/click"/> </RelativeLayout> </android.support.v7.widget.CardView> 

And here is the code that I use now to solve the problem (I don’t like this approach, I want to find a better way)

 public class PositionAwareCardView extends CardView { private int mChildPosition; public PositionAwareCardView(Context context) { super(context); } public PositionAwareCardView(Context context, AttributeSet attrs) { super(context, attrs); } public PositionAwareCardView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public void setChildPosition(int pos) { mChildPosition = pos; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // if it not the topmost view in view hierarchy then block touch events return mChildPosition != 0 || super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { return false; } } 

EDIT 2: I forgot to mention that this problem is only present on pre-Lolipop devices, it seems that starting with Lolipop, ViewGroups also take into account elevation when sending touch events

EDIT 3: Here is my current child drawing order:

 @Override protected int getChildDrawingOrder(int childCount, int i) { return childCount - i - 1; } 

EDIT 4: Finally, I was able to fix the problem thanks to a random user, and this answer , the solution was extremely simple:

 @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (!onInterceptTouchEvent(ev)) { if (getChildCount() > 0) { final float offsetX = -getChildAt(0).getLeft(); final float offsetY = -getChildAt(0).getTop(); ev.offsetLocation(offsetX, offsetY); if (getChildAt(0).dispatchTouchEvent(ev)) { // if touch event is consumed by the child - then just record it coordinates x = ev.getRawX(); y = ev.getRawY(); return true; } ev.offsetLocation(-offsetX, -offsetY); } } return super.dispatchTouchEvent(ev); } 
+6
source share
2 answers

CardView uses the elevation API to L and up to L, it resizes the shadow. dispatchTouchEvent in L respects the ordering of Z when iterating over children that does not occur in pre L.

Looking at the source code:

Pre lollipop

ViewGroup # dispatchTouchEvent

 public boolean dispatchTouchEvent(MotionEvent ev) { final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; final View child = children[childIndex]; ... 

Lollipop

ViewGroup # dispatchTouchEvent

 public boolean dispatchTouchEvent(MotionEvent ev) { // Check whether any clickable siblings cover the child // view and if so keep track of the intersections. Also // respect Z ordering when iterating over children. ArrayList<View> orderedList = buildOrderedChildList(); final boolean useCustomOrder = orderedList == null && isChildrenDrawingOrderEnabled(); final int childCount = mChildrenCount; for (int i = childCount - 1; i >= 0; i--) { final int childIndex = useCustomOrder ? getChildDrawingOrder(childCount, i) : i; final View sibling = (orderedList == null) ? mChildren[childIndex] : orderedList.get(childIndex); // We care only about siblings over the child if (sibling == child) { break; } ... 

The order of detailed drawing can be overridden by the user order of detailed drawing in the ViewGroup and setZ (float) custom Z values ​​set in the views.

You might want to check out the custom detailed drawing order in the ViewGroup , but I think your current fix for the problem is good enough.

+2
source

Have you tried to set the top view as interactive?

 button.setClickable(true); 

If this attribute is not set (by default) in the XML view, it propagates a click-up event.

But if you set it to the top view as true, it should not propagate any event on any other view.

0
source

Source: https://habr.com/ru/post/989684/


All Articles