在重叠视图的情况下控制触摸事件传播

Controlling touch event propagation in case of overlapping views

我在尝试控制 RecycleView 中的触摸事件传播时遇到了一些问题。所以我有一个 RecyclerView 填充一组模仿卡片堆的 CardView(因此它们以一定的位移相互重叠,尽管它们具有不同的高程值)。我目前的问题是每张卡片都有一个按钮,并且由于卡片的相对位移小于按钮的高度,因此会导致按钮重叠的情况,并且无论何时调度触摸事件,它都会从视图层次结构的底部开始传播(来自 children 最高 child 数量)。

根据我阅读的文章(this, this and also this video) touch propagation is dependent on the order of views in parent view, so touch will first be delivered to the child with highest index, while I want the touch event to be processed only by touchables of the topmost view and RecyclerView (it also has to process drag and fling gestures). Currently I am using a fallback with cards that are aware of their position within parent's view hierarchy to prevent wrong children from processing touches but this is really ugly way to do that. My assumption is that I have to override dispatchTouchEvent RecyclerView 的方法并正确地将触摸事件发送到最顶层 child。但是,当我尝试这种方式时(这也很好笨拙):

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

只有 DOWN 事件被传送到卡片中的按钮(没有触发点击事件)。对于反转触摸事件传播顺序的方式或将触摸事件委托给特定视图的任何建议,我将不胜感激。非常感谢您。

编辑:这是示例卡组的截图

示例适配器代码:

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

    }
}

以及卡片布局示例:

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

这是我现在用来解决问题的代码(我不喜欢这种方法,我想找到更好的方法)

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's 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;
}
}

编辑 2:我忘记说了,这个问题只存在于 pre-Lolipop 设备上,似乎从 Lolipop 开始,ViewGroups 在调度触摸事件时也会考虑高度

编辑 3:这是我当前的 child 绘图顺序:

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

编辑 4:由于用户随机,我终于能够解决问题,this answer,解决方案非常简单:

@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's coordinates
                x = ev.getRawX();
                y = ev.getRawY();
                return true;
            }
            ev.offsetLocation(-offsetX, -offsetY);
        }
    }
    return super.dispatchTouchEvent(ev);
}

您是否尝试将最顶部的视图设置为可点击?

button.setClickable(true);

如果视图中未设置此属性(默认设置)XML,它会向上传播点击事件。

但是,如果您在最顶层的视图中将其设置为 true,则它不应在任何其他视图中传播任何事件。

CardView 在 L 上和 L 之前使用高程 API,它会更改阴影大小。 dispatchTouchEvent 在 L 中迭代子代时尊重 Z 顺序,这在 L 之前不会发生。

查看源码:

前棒棒糖

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];

...

棒棒糖

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

可以使用在视图上设置的 custom child drawing order in a ViewGroup, and with setZ(float) 自定义 Z 值覆盖子绘图顺序。

您可能想查看 custom child drawing order in a ViewGroup,但我认为您当前对问题的修复已经足够好了。