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

Posted

技术标签:

【中文标题】在重叠视图的情况下控制触摸事件传播【英文标题】:Controlling touch event propagation in case of overlapping views 【发布时间】:2015-06-24 14:41:45 【问题描述】:

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

根据我阅读的文章(this,this 和 this video)触摸传播依赖于父视图中视图的顺序,因此触摸将首先传递给索引最高的子视图,而我希望触摸事件仅由最顶层视图和 RecyclerView 的可触摸对象处理(它还必须处理拖放手势)。目前,我正在使用知道其在父视图层次结构中的位置的卡片的后备,以防止错误的子级处理触摸,但这确实是一种丑陋的方式。我的假设是我必须重写 RecyclerView 的 dispatchTouchEvent 方法,并仅将触摸事件正确地分派给最顶层的孩子。但是,当我尝试这种方式时(这也有点笨拙):

    @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_
          android:layout_>
<RelativeLayout android:layout_
                android:layout_>

    <TextView android:layout_
              android:layout_
              android:layout_centerInParent="true"
              android:layout_margin="16dp"
              android:textSize="24sp"
              android:id="@+id/number"/>

    <TextView android:layout_
              android:layout_
              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:我忘了说,这个问题只存在于Lolipop之前的设备上,似乎从Lolipop开始,ViewGroups在调度触摸事件时也考虑了海拔

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

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

【问题讨论】:

你能发布你的xml和适配器吗 @war_Hero 我添加了一个测试应用程序的代码,该应用程序与问题中描述的问题相同 【参考方案1】:

CardView 在 L 和 L 之前使用海拔 API,它改变了阴影大小。 L 中的dispatchTouchEvent 在迭代子项时尊重 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 和setZ(float) 在视图上设置自定义Z 值覆盖子绘图顺序。

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

【讨论】:

感谢您的回复,是的,我已经覆盖了子绘图顺序(您可以在我上次编辑中看到代码),因为在 Lolipop 之前的设备中,我遇到了子放置不正确的问题。所以,不幸的是,我不能再更改子绘图顺序,否则我会遇到一些渲染问题,所以我一直在寻找覆盖 dispatchTouchEvent 和控制触摸调度顺序的解决方案。我不喜欢我目前的解决方案,因为在我看来,卡片不应该知道它们在视图中的位置,我宁愿在 LayoutManager 和 RecyclerView 类中包含与位置相关的所有内容。 没错,但是棒棒糖前的版本不尊重 z 顺序,所以除了用你已经在做的自定义实现覆盖默认行为之外,不要认为你可以做很多事情跨度> 我认为必须有一种方法可以覆盖dispatchTouchEvent,这将严格限制触摸传播到视图层次结构中的第一个视图,不幸的是我自己的努力并没有带来太多的利润字段,我尝试了最愚蠢的方法,例如直接在最顶层的孩子上调用调度触摸事件,但我失去了 RecyclerView 的触摸响应能力,所以我尝试只将触摸事件调度到最顶层的孩子的可触摸事件,但我遇到了这个奇怪的问题当并非所有触摸事件都传递到按钮时(问题中描述的那个) 我在这里从 seb ***.com/questions/9586032/… 找到了一个很好解释的答案,它告诉您 ViewGroup 如何处理触摸以及调用 dispatchTouchEvent 和 onInterceptTouch 的顺序。看看它是否澄清了什么 哦,我真不敢相信我是那么愚蠢,并且没有注意到当我在查看ViewGroup的源代码时,现在回头一看我知道如何解决这个问题,感谢您指出正确的做法【参考方案2】:

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

button.setClickable(true);

如果在 View XML 中没有设置这个属性(默认),它会向上传播点击事件。

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

【讨论】:

每当您在视图可点击标志上设置 OnClickListener 时,都会自动设置。我尝试手动执行此操作 - 它没有帮助。如果我将除最顶部之外的每个视图的 clickable 设置为 false,这可能会有所帮助,但基本上这将是我现在拥有的几乎相同的解决方案,同时我正在寻找一种控制触摸事件传播的正确方法

以上是关于在重叠视图的情况下控制触摸事件传播的主要内容,如果未能解决你的问题,请参考以下文章

如何检查多个 UIView 子视图相互重叠显示的贝塞尔路径内的触摸事件?

如何在不丢失触摸事件的情况下冻结 UIScrollView 滚动

将触摸事件传递给 UICollectionViewCell 内容?

iOS 在容器内移动视图会混淆触摸区域

将触摸事件从视图发送到控制器

UIResponder 链题