防止 RecyclerView 消费触摸事件

Preventing RecyclerView from consuming touch events

问题

我正在尝试模拟 Google Play 应用程序的确切行为。

当前,当您在 'Top Games' 之类的类别中滚动并触摸列表时,该单元格以及 RecyclerView 处理触摸事件,您可以看出它们都在处理它由于当您按住手指时出现波纹以及滚动停止。或者,如果您在列表仍在移动时单击某个单元格,该内容会立即打开。

默认行为是,如果您在滚动时将手指向下放置,则滚动会立即停止,然后它会消耗该触摸事件,这意味着该单元格永远不会被告知该触摸。当您的应用程序主要内容位于 RecyclerView 中并且如果用户连续滚动然后单击单元格时,这会令人难以置信地恼火,这需要他们单击两次,因为第一次单击已因停止滚动而消耗,即使如果列表几乎没有移动。

到目前为止我已经尝试过什么

起初我认为 RecyclerView 中一定有某种标志告诉它不要消耗触摸事件。我查看了 Android 源代码并浏览了 Android 文档,但无济于事。

然后我尝试用 onInterceptTouchEvent 捕获触摸事件并手动告诉我的视图它正在被触摸;

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    View myCell = recyclerView.findChildViewUnder(event.getX(), event.getY());
    myCell.dispatchTouchEvent(event);
    return super.onInterceptTouchEvent(event);
}

在这里,dispatchTouchEvent 似乎没有做任何事情,即使我得到的视图具有我在适配器中设置的相应标签。

我试过用我的代码机枪从各个角度解决这个问题;玩 requestDisallowInterceptTouchEvent,手动调用 onTouch 并覆盖 RecyclerView 及以后的所有监听器。

似乎没有任何效果。我确定必须有一个优雅的解决方案,我已经在精神错乱的道路上的某个地方浏览了一个重要的细节?特别是因为 Google 自己的应用程序以这种方式运行。

请帮忙! :) 谢谢。

解决方案

感谢 Jagoan Neon 找到了解决方案。为了便于使用,我最终把他的答案包装在自定义的 RecyclerView 中。

public class MultiClickRecyclerView extends RecyclerView {
   public MultiClickRecyclerView(Context context) {
        super(context);
   }

   public MultiClickRecyclerView(Context context, AttributeSet attrs) {
        super(context, attrs);
   }

   public MultiClickRecyclerView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
   }

   @Override
   public boolean onInterceptTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN && this.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING) {
            this.stopScroll();
        }
        return super.onInterceptTouchEvent(event);
   }
}

首先你应该定义一个 OnItemTouchListener,也许是像这样的全局变量:

RecyclerView.OnItemTouchListener mOnItemTouchListener = new RecyclerView.OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_DOWN && rv.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING) {
            Log.d(TAG, "onInterceptTouchEvent: click performed");
            rv.findChildViewUnder(e.getX(), e.getY()).performClick();
            return true;
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {}

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
};

为什么 ACTION_DOWN 和 SCROLL_STATE_SETTLING?

ACTION_DOWN motion 事件将在您对 RecyclerView 执行触摸时触发,并且在那个确切的时间点 RecyclerView 将其滚动状态更改为 SCROLL_STATE_SETTLING,因为它试图停止滚动。这正是您想要执行点击的时间。

并记住 return true 以便您告诉 RecyclerView 您已经使用了 MotionEvent。否则你的 performClick() 可能会被多次调用。

在 onResume 时将其添加到您的 RecyclerView 并在 onPause 时将其删除,一切就绪;)

更新

在执行 findChildViewUnder 时添加空值检查器 - 当您未接触特定单元格时,它 return 为空值。

View childView = rv.findChildViewUnder(e.getX(), e.getY());
if (childView != null) childView.performClick();

更新 2

事实证明,停止 RecyclerView 滚动就足够了。改为这样做:

@Override
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
        if (e.getAction() == MotionEvent.ACTION_DOWN && rv.getScrollState() == RecyclerView.SCROLL_STATE_SETTLING)
            rv.stopScroll();
        return false;
    }