热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

关于RecyclerView的事件拦截机制

今日科技快讯9月1日上午,五部门联合约谈11家网约车平台公司。约谈强调,各平台公司要排查自身问题,立即整改。要加快推进合规化,


/   今日科技快讯   /

9月1日上午,五部门联合约谈11家网约车平台公司。约谈强调,各平台公司要排查自身问题,立即整改。要加快推进合规化,制定切实可行的网约车合规化工作方案,明确时间表、目标和工作举措,特别是要提出到今年年底前,车辆、驾驶员合规化工作的具体目标。

/   作者简介   /

周五愉快,我们下周再见。

本篇来自我的猫叫冰彬的投稿,文章主要分享了的RecyclerView的事件拦截,相信会对大家有所帮助,同时也感谢作者贡献的精彩文章!

我的猫叫冰彬的博客地址:

https://blog.csdn.net/qq_43478882

/   前言   /

最近在利用RecyclerView做开发的时候,遇到了一点问题:

给RecyclerView的子项添加事件监听的时候,发现ACITON_DOWN能得到处理,ACITON_UP和ACTION_MOVE却得不到处理。

在刚开始开发需求的时候我还不太了解事件分发的机制。所以我先去学习了一下事件分发,这里对事件分发做一个简单的总结。

/   原因分析   /

事件分发的机制

事件分发是由三个方法配合完成的:

  • dispatchTouchEvent() 分发事件

  • onInterceptTouchEvent() 拦截事件

  • onTouchEvent() 处理事件

而且事件分发的顺序是:

Activity -> ViewGroup -> View

借助一张图来配合理解:

图源(https://www.cnblogs.com/aademeng/articles/6551337.html)

通过图片我们可以看到,ViewGroup是比较特殊的。onInterceptTouchEven()是他独有方法,他可以将事件拦截下来选择不分发给下一层的View而是自己处理。

原因猜测

在了解了事件分发的机制过后,我就猜测会不会是因为RecyclerView将事件拦截了下来。

因为RecyclerView肯定有他自己的事件监听,当ACTION_MOVE的时候应该会触发滚动,加载数据然后显示到屏幕上。

那如果真的是被RecyclerView给拦截了,那我又产生了新的疑问:

  • 根据事件分发的机制,在ACITON_DOWN的时候应该就决定了targetView是itemView,为什么在ACITON_MOVE的时候会目标View又变成了RecyclerView?

  • RecyclerView是怎么做到只拦截ACTION_MOVE和ACTION_UP而不拦截ACTION_DOWN的呢?

  • 那如果想要实现子项自己处理ACTION_MOVE和ACTION_UP要怎么处理呢?

为了验证我的猜想和解决这些疑问,我决定去RecyclerView的源码里一探究竟。

/   RecyclerView事件拦截   /

既然我们要分析的是拦截机制,那么当然应该去onInterceptTouchEvent这个方法里去看。

这里先说明一下,以下贴出来的源码并不是全部。我一直觉得分析源码不能一行一行的扣,不然思路会很混乱。

在这篇文章里,我只把对解决问题有用的部分贴了出来,也能让大家更好理解。如果有小伙伴有看不懂的地方,可以再配合所有源码来理解。

    @Overridepublic boolean onInterceptTouchEvent(MotionEvent e) {...mInterceptingOnItemTouchListener = null;if (findInterceptingOnItemTouchListener(e)) {cancelScroll();return true;}...final int action = e.getActionMasked();...switch (action) {case MotionEvent.ACTION_DOWN: {...} break...case MotionEvent.ACTION_MOVE: {...} break;...case MotionEvent.ACTION_UP: {..} break;...}return mScrollState == SCROLL_STATE_DRGING;}

总的来说这个函数我把他分为两个部分,switch(aciton)之前和switch(aciton)部分。我们按倒序分析一下。

switch (action)部分

  • 返回值

这部分呢我们需要先看一下最后的返回值,因为返回值决定了是否拦截。

return mScrollState == SCROLL_STATE_DRGING;

解释一下mScrollState这个变量。这个变量是用来记录滑动状态的,有下面三个值:

//停止滚动
public static final int SCROLL_STATE_IDLE = 0;//正在被外部拖拽,一般为用户正在用手指滚动
public static final int SCROLL_STATE_DRAGGING = 1;//自动滚动开始
public static final int SCROLL_STATE_SETTLING = 2;

第一个和第二个都好理解,这里解释一下第三个状态。

整个RecyclerView里只有在fing()方法里会把mScrollState的值设置为SCROLL_STATE_SETTLING。而fling()这个函数呢,其实就是指当你手指在屏幕上快速滑动时,会触发自动滑动。就像下面这样:

这个功能其实大家日常使用中也经常会用到。大家知道这个状态的含义即可。

那这个返回值的意思就是判断最后RecyclerView是否是手指正在拖着滚动的状态。如果是正在滚动,那么就会拦截本次事件;反之则不拦截

  • ACTION_DOWN

进入到ACTION_DOWN操作,前面部分和后面部分都是设置一些状态(触点的位置,布局滚动方向是竖直的还是垂直的)。最重要的是中间的判断。

case MotionEvent.ACTION_DOWN:...if (mScrollState == SCROLL_STATE_SETTLING) {getParent().requestDisallowInterceptTouchEvent(true);setScrollState(SCROLL_STATE_DRAGGING);stopNestedScroll(TYPE_NON_TOUCH);}...break;

如果目前的状态是在自动滚动的状态下,里面就会将mScrollState设置为SCROLL_STATE_DRAGGING。

这里其实很好想明白。当你的列表在自动快速滚动的过程中,手指再按上去,是需要他立即停下来的。那么理所应当这里需要把事件拦截下来RecyclerView自己处理。就像下面这样就会拦截:

不拦截的话,就只能等他自己停下来,那这个自动滚动就是不可控的了。

那如果不是这种情况,便不会拦截。那么子项就可以接受到ACTION_DOWN事件啦。

  • ACTION_MOVE

 case MotionEvent.ACTION_MOVE: {...           final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);if (mScrollState != SCROLL_STATE_DRAGGING) {final int dx = x - mInitialTouchX;final int dy = y - mInitialTouchY;boolean startScroll = false;if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {mLastTouchX = x;startScroll = true;}if (canScrollVertically && Math.abs(dy) > mTouchSlop) {mLastTouchY = y;startScroll = true;}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}
} break;

ACTION_MOVE里面就很简单啦。

  1. 如果当前mScrollState的状态是正在滚动,那么就不做任何处理了。这个时候表示手指正在拖着列表滚动,自然是要拦截下来的。

  2. 如果当前mScrollState的状态不是滚动,那就会进行一个判断了。判断你手指的移动的距离是否在相应方向上超过了一个阈值。如果超过了这个阈值,说明你想要开始滑动了,那么这个时候又会调用setScrollState(SCROLL_STATE_DRAGGING)来将mScrollState的值设置为滚动,将事件拦截下来。

  • ACTION_UP

ACTION_UP里并没有对mScrollState进行修改和赋值。所以这个时候也就会根据是否正在滑动来判断是否拦截事件了。

  • 小结

RecyclerView确实会拦截事件,会对最基本的三个事件根据情况拦截:

  • ACTION_DOWN:当列表在自动滚动的状态下会拦截,用于处理停止滚动。

  • ACTION_MOVE:当手指移动的距离在对应方向上超过了阈值,就会拦截掉事件,用于列表滚动。

  • ACTION_UP :根据当前列表是否处于滚动状态选择是否拦截。

这部分的内容其实就已经能证实我们的猜想了。

switch (action) 之前

看到这里你可能会好奇,前面不是已经能证实猜想了吗?别急,在分析源码的时候我还发现一个东西,短短的几行代码,展现出RecyclerView的灵活性。这也就是为什么我要把这部分放到后面来说。

    mInterceptingOnItemTouchListener = null;if (findInterceptingOnItemTouchListener(e)) {cancelScroll();return true;}

我们先从mInterceptingOnItemTouchListener的类型OnItemTouchListener接口开始说起吧。

  • OnItemTouchListener接口

熟悉ListView的同学都知道,ListView可以通过setOnItemClickListener()来给一个ItemView添加事件的监听器,而RecyclerView并没有这样的方法。

那么你可能就有疑问了,为什么RecyclerView在各方面的设计都要优于ListView,偏偏在点击事件上却没有处理的非常好呢?其实不是这样的,ListView在点击事件上处理得并不人性化,setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮呢?虽然ListView也能做到,但是实现起来就相对比较麻烦了。为此,RecyclerView干脆直接摒弃了子项点击事件的监听器,让所有的点击事件都由具体的View去注册,就再没有这个困扰了。

(摘自郭霖《第一行代码》)

郭神在书中给出的方法,也是在Adapter的onCreateViewHolder()方法里去给每一个子项绑定事件监听,这样做确实更灵活,但同时因为每个子项都绑定了事件监听,在内存上也会有一定的消耗。其实RecyclerView内部也有一个接口,能够实现对整个RecyclerView的监听。

    public interface OnItemTouchListener {boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);}

前两个通过方法名我们可以轻易的猜出他们的目的,不就是ViewGroup里事件分发的两个函数吗。

我们重点说一下第三个函数。第三个函数在ViewGroup里也有实现,他的作用是设置ViewGroup是否开启事件拦截。也解释说,通过这个函数我们可以在子View里设置父ViewGroup关闭拦截,这样就能让子View自行处理事件了。

An OnItemTouchListener allows the application to intercept touch events in progress at the view hierarchy level of the RecyclerView before those touch events are considered for RecyclerView's own scrolling behavior.

This can be useful for applications that wish to implement various forms of gestural manipulation of item views within the RecyclerView.

OnItemTouchListeners may intercept a touch interaction already in progress even if the RecyclerView is already handling that gesture stream itself for the purposes of scrolling.

翻译一下,大概意思就是这个监听器允许在RecyclerView考虑自己的滚动事件之前,在ViewGroup层面拦截事件。

说人话就是RecyclerView在处理事件的时候,得先看这个监听器要不要拦截这个事情,如果监听器要拦截,那么RecyclerView就没资格自己处理了。

实现了对RecyclerView整个视图的监听,允许我们自定义对一些特定手势的处理。

用这个接口有什么好处呢?

1. 节省内存。在运行期间只有一个监听器,不像之前RecyclerView的每个子项都要设置一个监听器。

2. 对于整个面板来说更加灵活。如果说我们需要对整个面板有一些自定义的手势操作,那么就只能通过实现这个接口,去子项里实现已经不太可能了。

  • 拦截过程

因为本文篇幅原因,就不展示怎么去实现了。我们这里通过源码分析一下他是如何做到让RecyclerView没资格处理自己的滚动的。

    private final ArrayList mOnItemTouchListeners &#61;new ArrayList<>();private OnItemTouchListener mInterceptingOnItemTouchListener;

首先是有两个全局变量&#xff0c;一个用来存放所有的自定义实现的OnItemTouchListener。我们可以通过调用addOnItemTouchListener()来添加监听器。这里也说明了一个RecycerView里可以自定义多个监听器。另一个是用来记录拦截事件的监听器。可能这里有点懵&#xff0c;看到下面就能明白了。

public void addOnItemTouchListener(&#64;NonNull OnItemTouchListener listener) {mOnItemTouchListeners.add(listener);
}

然后我们回到RecyclerView的onInterceptTouchEvent()那五行代码

    mInterceptingOnItemTouchListener &#61; null;if (findInterceptingOnItemTouchListener(e)) {cancelScroll();return true;}

先将mInterceptingOnItemTouchListener置为null&#xff0c;是为了避免上一次赋值的mInterceptingOnItemTouchListener没有被销毁&#xff0c;导致出错。

然后我们到findInterceptingOnItemTouchListener()方法里去看看。

    private boolean findInterceptingOnItemTouchListener(MotionEvent e) {int action &#61; e.getAction();final int listenerCount &#61; mOnItemTouchListeners.size();for (int i &#61; 0; i < listenerCount; i&#43;&#43;) {final OnItemTouchListener listener &#61; mOnItemTouchListeners.get(i);if (listener.onInterceptTouchEvent(this, e) && action !&#61; MotionEvent.ACTION_CANCEL) {mInterceptingOnItemTouchListener &#61; listener;return true;}}return false;}

这里的逻辑非常简单&#xff0c;去遍历监听器数组&#xff0c;如果发现其中一个监听器拦截了此类事件并且事件不是ACTION_CANCEL &#xff0c;那么就给mInterceptingOnItemTouchListener赋值。这里说明了mInterceptingOnItemTouchListener的用处&#xff0c;记录了拦截事件的监听器。

然后如果找到了这么一个监听器&#xff0c;返回true&#xff0c;那么RecyclerView就会取消滚动&#xff0c;帮监听器直接拦截下本次事件&#xff0c;确保不会往下分发。从这里就能看出这个监听器的优先级了。

而对事件的处理的入口呢&#xff0c;是在RecyclerView的onTouchEvnet()里面。

    &#64;Overridepublic boolean onTouchEvent(MotionEvent e) {...if (dispatchToOnItemTouchListeners(e)) {cancelScroll();return true;}...}

这里调用的是dispatchToOnItemTouchListeners()这个方法

      private boolean dispatchToOnItemTouchListeners(MotionEvent e) {if (mInterceptingOnItemTouchListener &#61;&#61; null) {if (e.getAction() &#61;&#61; MotionEvent.ACTION_DOWN) {return false;}return findInterceptingOnItemTouchListener(e);} else {mInterceptingOnItemTouchListener.onTouchEvent(this, e);final int action &#61; e.getAction();if (action &#61;&#61; MotionEvent.ACTION_CANCEL || action &#61;&#61; MotionEvent.ACTION_UP) {mInterceptingOnItemTouchListener &#61; null;}return true;}}

在这个方法里&#xff0c;如果mInterceptingOnItemTouchListener不为空&#xff0c;那么就在这里调用它的onTouchEvent()去处理。返回了true&#xff0c;ReyclerView自然就不会自己处理了。

  • 小结

这个就是自定义的onItemTouchListener的拦截过程了。

  1. 我们自定义实现的onItemTouchListener&#xff0c;需要通过addOnItemTouchListener()添加到RecyclerView里。

  2. RecyclerView在判断拦截事件时&#xff0c;会优先判断有没有自定义的onItemTouchListener要拦截此次事件&#xff0c;如果有&#xff0c;则会帮他拦截下来。

  3. RecyclerView在处理事件时&#xff0c;也会优先判断判断有没有自定义的onItemTouchListener要处理该次事件。如果有&#xff0c;那就交给它处理&#xff0c;自己不再处理。

/   解决问题   /

在分析完整个拦截机制后&#xff0c;我们就可以有两套解决方案了。具体方案可以根据需求自行选择。

方案一

这种方案推荐用于针对子项的某一具体组件的事项&#xff0c;比如RecyclerView的子项是一个RelativeLayout&#xff0c;事件只针对其中的一个Button。

1. 如果子View不需要自己处理ACTION_MOVE&#xff0c;只需要在ACTION_UP里做一些收尾操作&#xff0c;那么可以把收尾操作添加一份到ACTION_CANCEL里。

2. 如果子View需要自己处理ACTION_MOVE和ACTION_UP&#xff0c;那么就可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)来设置不让RecyclerView对事件进行拦截。不过这种方法不建议添加到ACTION_DOWN里&#xff0c;会导致列表无法滑动。

方案二

这种方案用于针对子项或者整个RecyclerView的事件。

通过实现onItemTouchListener接口来处理自己需要的事件&#xff0c;通过手指按下的位置获取到具体的子项。

/   总结   /

做一个整体的总结。

1. ReyclerView的ItemView的事件&#xff0c;特别是ACTION_MOVE和ACTION_UP容易被RecyclerView拦截&#xff0c;但是会发送一个ACTION_CANCEL给子View用来处理一些收尾工作。

2. 如果ItemView不希望被RecyclerView给拦截&#xff0c;可以通过parent.requestDisallowInterceptTouchEvent(true)来设置&#xff0c;这样就不会被拦截。

3. RecyclerView提供了一个内部接口onItemTouchListener用于对整个RecyclerView进行监听&#xff0c;可以实现更灵活的功能&#xff0c;优先级高于RecyclerView自己的事件处理。

最后&#xff0c;非常感谢你可以看到这里。我是一个即将大四的实习生&#xff0c;Android的知识体系还没有成为一个牢固的系统&#xff0c;在这之前我甚至都不知道什么是事件分发。本篇内容全是自己学习相关知识后读源码的理解&#xff0c;难免会有差错。如果发现了错误希望大家谅解并为我指出错误。也非常希望这篇文章能给你带来一点帮助。

推荐阅读&#xff1a;

我的新书&#xff0c;《第一行代码 第3版》已出版&#xff01;

如何优雅的在业务中使用设计模式

新版Glance发布&#xff0c;更好用的Android数据库调试助手

欢迎关注我的公众号

学习技术或投稿

长按上图&#xff0c;识别图中二维码即可关注


推荐阅读
author-avatar
toto333
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有