前言

在Android的开发中,不可避免的需要用到列表嵌套列表的需要,如recycleView嵌套recylerView,我们就会发现被嵌套的列表会出现滑动冲突

这是一个简单的recyclerView嵌套recyclerView的demo,
很明显,子布局应该也是可以滑动的才对,但你滑动子布局却是父布局在滑动
这就是滑动冲突

事件分发机制

要向解决滑动冲突问题让子布局正常使用我们需要先了解一下Android的事件分发机制

点击事件的传递规则

首先我们要明白我们分析的对象是MotionEvent,即点击事件
点击事件就是手指接触屏幕后所产生的一系列事件:

  • ACTION_DOWN 手指刚接触屏幕
  • ACTION_MOVE 手指在屏幕上移动
  • ACTION_UP 手指从屏幕上松开那一瞬间

所谓点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生之后,系统需要把这个事件传递给一个具体的View,而这个传递就是分发过程。

下面来介绍下点击事件分发3个很重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回的结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件

public boolean onInterceptTouchEvent(MotionEvent ev)
dispatchTouchEvent(MotionEvent ev)方法内部调用,用来判断是否拦截某个事件,那么在同一个事件系列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

public boolean onTouchEvent(MotionEvent ev)
dispatchTouchEvent(MotionEvent ev)方法中调用,用来处理点击事件,返回的结果表示是否消耗当前事件,如锅不消耗,这在同一个事件系列中,当前View无法再接收到事件。

那么上面的三个方法有什么区别吗?好像有点乱,我们用一段伪代码说明规则来理清下逻辑吧

public boolean dispatchTouchEvent(MotioEvent ev){       boolean consume = false;       if(onInterceptTouchEvent(ev)){           consume = onTouchEvent(ev);       }else{           consume = child.dispatchTouchEvent(ev);       }       return consume;}

通过上面的伪代码,点击事件传递的大致规则我们也有说了解了:
对于一个根ViewGroup来说,点击事件产生后,首先会先传递给它,这是它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;
如果这个ViewGroup的onInterceptTouchEvent的方法返回false,就表示它不拦截当前事件,这时当前事件就会继续传递给他的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此重复直到事件被最终处理

除此之后,再补充几条事件分发的规则:

  • 当一个要处理事件的View设置了onTouchListener,那么onTouchListener里的onTouch方法会被回调
    这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent就会被调用;如果返回true,那么onTouchEvent将不会被调用。由此可见,给View设置onTouchListener,其优先级比onTouchEvent要高。在onTouchEvent方法里,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,平时我们使用的OnClickListener,其优先级最低,即处于点击事件的最尾端。

  • 当一个点击事件产生后,它的传递过程遵以下顺序:Activity -> Window -> View,即事件总是先传递给Activity,Activity再传递给Window,最后Window在传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件
    考虑一种情况,如果一个View的onTouchEvent返回false,那么它父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事情,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法将会被调用

  • 正常情况下,一个事件序列只能被一个View拦截且消耗
    因为一但一个元素拦截了某此事件,那么同一个事件序列的所有事情都会直接教给它处理,因此同一个事件序列中的事件不能分别由2个View同时处理,但是可以通过特殊手段做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理

  • 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用
    当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都交给它来处理,因此就不会调用这个View的onInterceptTouchEvent来询问是否要拦截了

  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一个事件序列的其他事件都不会再交给它来处理,并且将事件重新交由它的父元素去处理,即父元素的onTouchEvent会被调用
    意思就是事件一但交给了一个View处理,那么它必须消耗掉,否则同一事件序列下剩下的事件就不再交给它来处理了,这好比是上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员来做了。

  • 如果View不消耗ACTION_DOWN以外的事件,那么这个点击事件也会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理

  • ViewGroup默认不拦截任何事件
    Android源码中的ViewGroup的onInterceptTouchEvent方法默认返回false

  • View没有onInterceptTouchEvent方法,一旦由点击事件传递给他,那么它的onTouchEvent方法就会被调用

  • View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)
    View的longClickable属性默认都为false,clickable是要很情况的,如Button的clickable默认为true,而TextView的为false

  • View的enable属性不影响onTouchEvent的默认返回值
    哪怕一个View是disable状态的额,只要他的clickable和longClickable由一个为true,那么他的onTouchEvent就返回true

  • 事件的分发是由内外向内的,即事件总是向传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程
    但是ACTION_DOWN事件除外

原因分析

通过事件分发的原理我们知道,子recyclerView不可滑动的原因是因为点击事件被父recyclerView给消耗掉了
那么就得向方法让子recyclerView拿到点击事件

解决方案一

父recyclerView拦截并消耗了点击事件,那么就不要让父recyclerView拦截呗
自定义父recyclerView并重写onInterceptTouchEvent()方法

public class ParentRecyclerView extends RecyclerView {    public ParentRecyclerView(@NonNull Context context) {        super(context);    }    public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    //不拦截,继续分发下去    @Override    public boolean onInterceptTouchEvent(MotionEvent e) {        return false;    }}

然后用这个ParentRecyclerView 代替原来的RecyclerView(父布局那个)


解决方案二

子布局通知父布局不要拦截事件,通过requestDisallowInterceptTouchEvent方法干预事件分发过程
重写dispatchTouchEvent()方法,通知通知父层ViewGroup不要拦截点击事件

public class ChildPresenter extends RecyclerView {    public ChildPresenter(@NonNull Context context) {        super(context);    }    public ChildPresenter(@NonNull Context context, @Nullable AttributeSet attrs) {        super(context, attrs);    }    public ChildPresenter(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {        super(context, attrs, defStyle);    }    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        //父层ViewGroup不要拦截点击事件         getParent().requestDisallowInterceptTouchEvent(true);        return super.dispatchTouchEvent(ev);    }}

然后用这个ParentRecyclerView 代替用来的RecyclerView(子布局那个)


解决方案三

通过事件分发规则我们知道,OnTouchListener优先级很高,可以通过这个来告诉父布局,不要拦截我的事件

   holder.recyclerView.setOnTouchListener { v, event ->            when(event.action){                //当用户按下的时候,我们告诉父组件,不要拦截我的事件(这个时候子组件是可以正常响应事件的),拿起之后就会告诉父组件可以阻止。                MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)                MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)            }            false}

总结

虽然这三种解决方案都做到了把事件传递给子布局,但具体效果还是由些许不同的
深入理解了事件分发机制就能找到是为什么了,这里就不再细谈

更多相关文章

  1. Android基础 : Android(安卓)Service[转]
  2. Android(安卓)HTTP实例 使用GET方法和POST方法发送请求
  3. Android(安卓)SQLite教程:内部架构及SQLite使用办法
  4. 利用WebView通过javascript调用android java方法
  5. Android自定义控件系列九:从源码看Android触摸事件分发机制
  6. Android8.0 存储系统
  7. 条件数据库Android:sqllite的简单使用
  8. Android线程模型(Painless Threading) --- 转
  9. Android(安卓)JSON:Gson,FastJson解析库的使用和对比分析

随机推荐

  1. Android中使用AndroidX出现 but the 'and
  2. Android View measure (五) 支持margin属性
  3. Android Checkbox在对话框中显示
  4. Android Bitmap 改变大小
  5. java android maven环境变量基本配置
  6. Android 自定义Tabbar
  7. inputstream ,outputstream,AssetManager
  8. android XML文件常用字符转义
  9. Android中kotlin的学习(anko + kotlin)
  10. android 适配全面屏手机