每日一问:Android 如何解决滑动冲突

作者 : 开心源码 本文共3938个字,预计阅读时间需要10分钟 发布时间: 2022-05-13 共189人阅读

坚持原创日更,短平快的 Android 进阶系列,敬请直接在微信公众号搜索:nanchen,直接关注并设为星标,精彩不容错过。

在 Android 开发中,滑动冲突总是我们一个无法避免的话题。而对于处理方案却是众说纷纭。比方 RecyclerView 嵌套 RecyclerView,直接通过相关方法禁掉内部 RecyclerView 的滑动;ScrollView 嵌套 RecyclerView 直接把 ScrollView 替换为 NestedScrollView 等等。但我们今天要说的是在自己设置 View 中遇到滑动冲突时,我们又应该如何解决呢?

当然,今天的话题需要 View 的事件分发机制做理论前提,还不理解 View 的事件分发机制的小伙伴可以移步我之前面试系列的一篇文章:面试系列:讲讲 Android 的事件分发机制。

简单详情 View 的时间分发机制

当然,这里也可以简单地提一下,基本的流程就是下面的伪代码。

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

当一个 ViewGroup 接收到一个事件的时候,首先会调用 dispatchTouchEvent() 方法进行事件分发,假如 onInterceptTouchEvent() 返回 true,则代表当前 View 会阻拦事件,则直接回调 onTouchEvent() 方法进行事件解决。假如不阻拦,则直接回调子 View 的 dispatchTouchEvent() 方法,如此反复,一直到最里面的子 View。

当一个点击事件产生后,它的传递过程遵循以下顺序:Activity => Window => View,即事件总是先传递给 ActivityActivity 再传递给 Window,最后 Window 再传递给顶层 DocorView,而后遵循上面的方式一直在最里层 View

而解决事件则从最里层 View 不断回传给自己的外层 View,假如一直没有 View 进行解决,则直接会回传到 Activity 中。

onTouchEvent() 返回 true 代表自己要解决。

既然都提了这么一点,也就忽然想给出少量结论,参考自 Android 开发艺术探究:

  1. 同一个事件序列是指从手指接触屏幕(ACTION_DOWN)的那一刻起,到手指离开屏幕(ACTION_UP)的那一刻结束,中间含不定数量的 ACTION_MOVE 事件。
  2. 某个 View 一旦决定阻拦事件,那么这一个事件序列都只能由它解决,并且它的 onInterceptTouchEvent() 方法也不会再调用。换句话说,比方一个 ViewGroup 里面有数个子 View,一旦 ACTION_DOWN 事件从 Activity 传到这个 ViewGroup 被其阻拦,则后续的 MOVE 和 UP 等事件也不会传递到里面的子 View 中。
  3. 假如一个 View 一旦开始解决事件,假如它不消耗 ACTION_DOWN 事件,即 onTouchEvent() 返回为 false,那么同一事件序列中的其余时间也不会再交给它解决,直接会调用其父 View 的 onTouchEvent()
  4. 假如 View 不消耗除 ACTION_DOWN 以外的其余事件,那么这个点击事件会消失,此时父元素的 onTouchEvent() 并不会被调用,并且当然 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 解决。
  5. ViewGroup 默认不阻拦事件,View 没有 onInterceptTouchEvent() 方法,一旦有事件传递给它,则直接会调用 onTouchEvent(),并且起默认都会消耗掉事件。除非它是不可点击的(即 clickablelongClickable 均为 false)。View 的 longClickable 默认都为 false,而 clickable 分情况,比方 Button 默认为 trueTextView 默认为 false
  6. View 的 enable 属性不会影响 onTouchEvent() 的默认返回值,哪怕一个 Viewdisable 状态的,只需它的 clickable 或者者 longClickable 有一个为 true,那么它的 onTouchEvent() 就会返回 true
  7. requestDisallowInterceptTouchEvent() 可以在子元素中干预父元素的事件分发过程,但是无法干预 ACTION_DOWN 事件。
  8. 事件优先顺序:setOnTouchListener() => onTouchEvent() => onClickListener()

一不小心发现还是挺多的,当然这些都是结论,具体可以跟着 面试系列:讲讲 Android 的事件分发机制 进行源码流程讨论,你会发现上面的结论很容易得到。

解决自己设置 View 中的滑动冲突

对于大多数 Android 开发来说,解决滑动冲突如同很难,但实战一下又发现,如同也挺简单,由于这个实际上是有套路可循的。基本就两种方案:外部阻拦法 && 内部阻拦法。

外部阻拦法

所谓外部阻拦法,顾名思义,就是直接在父容器中直接阻拦掉我们的滑动事件,让其不能进入到子元素中,这似乎和我们 RecyclerView 嵌套 RecyclerView 时禁用内部 RecyclerView 滑动有那么一丝类似之处,就是内部不解决就完事儿了。但细细品来又完全不一样,这里的外部阻拦法会让内部元素根本就收不到滑动事件。

这种方法显著非常适合我们上面讲的事件分发机制。我们在接收 ACTION_MOVE 事件的时候,直接通过使 onInterceptTouchEvent() 方法返回 true 来直接阻拦掉事件即可以了,伪代码想必大家也知道了:

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {    ev?.run {         if (action == MotionEvent.ACTION_MOVE && 父容器需要点击事件){            return true        }    }    return super.onInterceptTouchEvent(ev)}

代码很简单,我们仅仅需要在事件 ACTION_MOVE 时去解决我们的逻辑就好了,当满足我们的逻辑的时候,就阻拦掉 ACTION_MOVE 事件给自己解决。

至于为什么不去阻拦 ACTION_DOWNACTION_UP,想必大家也清楚了。上面说了,假如阻拦了 ACTION_DOWN 事件,那后续的 ACTION_MOVEACTION_UP 等其它事件均不会在调用 onInterceptTouchEvent() 方法,会直接交给当前容器解决。而假如我们阻拦掉 ACTION_UP 的话,一定会导致子元素的点击事件无法被解决,由于大家一定都知道一个点击事件从 ACTION_DOWN 开始,从 ACTION_UP 结束,二者缺一不可。

内部阻拦法

内部阻拦法相对外部阻拦法会复杂少量,所以我们通常来说,都更加推荐用外部阻拦法进行解决。不过,内部阻拦法仍然有着它非常重要的地位,具体情况有可能会遇到。

内部阻拦法的话,需要 requestDisallowInterceptTouchEvent() 方法的支持,这个方法是干什么的呢?顾名思义,请求能否不允许阻拦事件,其接收一个 boolean 参数,表示能否不允许阻拦。

我们直接重写子元素的 dispatchTouchEvent() 方法,得到伪代码如下:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {    ev?.run {         when(action){            MotionEvent.ACTION_DOWN -> parent.requestDisallowInterceptTouchEvent(true)            MotionEvent.ACTION_MOVE ->{                if(满足需要让外部容器阻拦事件){                    parent.requestDisallowInterceptTouchEvent(false)                }            }        }    }    return super.dispatchTouchEvent(ev)}

想必代码也是非常简单易懂的,我们给父容器的 requestDisallowInterceptTouchEvent() 传递的参数代表能否不允许其阻拦事件,当参数为 true 的时候代表不允许阻拦,为 false 的时候代表阻拦。所以看起来和外部阻拦法也就如出一辙了。

不过仅仅有这点修改还不够,我们通过前面的理论基础知道,当我们的父容器阻拦掉 ACTION_DOWN 事件的时候,所有的事件都无法再传递到子元素中,自然也就不会调用上面我们写的 dispatchTouchEvent() 方法了。所以我们在内部阻拦法的时候还需要重写父容器的 onInterceptTouchEvent() 方法。

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {    ev?.run {         if (action == MotionEvent.ACTION_DOWN){            return false        }    }    return super.onInterceptTouchEvent(ev)}

至此,基本详情了两种解决滑动冲突的处理方案,在自己设置 View 的时候结合实际场景也即可以得心应手了。

除了滑动冲突,滑动解决也是一项非常有意思的工作,感兴趣的可以可以参考 NestedScrollingParent2 和 NestedScrollingChild2 哟。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 每日一问:Android 如何解决滑动冲突

发表回复