在 Android 开发中,滑动冲突老是咱们一个没法避免的话题。而对于解决方案倒是众说纷纭。好比 RecyclerView
嵌套 RecyclerView
,直接经过相关方法禁掉内部 RecyclerView
的滑动;ScrollView
嵌套 RecyclerView
直接把 ScrollView
替换为 NestedScrollView
等等。但咱们今天要说的是在自定义 View 中遇到滑动冲突时,咱们又应该如何处理呢?android
固然,今天的话题须要 View 的事件分发机制作理论前提,还不了解 View 的事件分发机制的小伙伴能够移步以前面试系列的一篇文章:面试系列:讲讲 Android 的事件分发机制。面试
固然,这里也能够简单地提一下,基本的流程就是下面的伪代码。ide
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。spa
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity
=> Window
=> View
,即事件老是先传递给 Activity
,Activity
再传递给 Window
,最后 Window
再传递给顶层 DecorView
,而后遵循上面的方式一直在最里层 View
。3d
而处理事件则从最里层 View
不断回传给本身的外层 View
,若是一直没有 View
进行处理,则直接会回传到 Activity
中。code
onTouchEvent()
返回true
表明本身要处理。blog
既然都提了这么一点,也就忽然想给出一些结论,参考自 Android 开发艺术探索:事件
- 同一个事件序列是指从手指接触屏幕(ACTION_DOWN)的那一刻起,到手指离开屏幕(ACTION_UP)的那一刻结束,中间含不定数量的
ACTION_MOVE
事件。- 某个 View 一旦决定拦截事件,那么这一个事件序列都只能由它处理,而且它的
onInterceptTouchEvent()
方法也不会再调用。换句话说,好比一个 ViewGroup 里面有数个子 View,一旦 ACTION_DOWN 事件从 Activity 传到这个 ViewGroup 被其拦截,则后续的 MOVE 和 UP 等事件也不会传递到里面的子 View 中。- 若是一个 View 一旦开始处理事件,若是它不消耗 ACTION_DOWN 事件,即
onTouchEvent()
返回为 false,那么同一事件序列中的其余事件也不会再交给它处理,直接会调用其父 View 的onTouchEvent()
。- 若是 View 不消耗除 ACTION_DOWN 之外的其余事件,那么这个点击事件会消失,此时父元素的
onTouchEvent()
并不会被调用,而且固然 View 能够持续收到后续的事件,最终这些消失的点击事件会传递给Activity
处理。- ViewGroup 默认不拦截事件,View 没有
onInterceptTouchEvent()
方法,一旦有事件传递给它,则直接会调用onTouchEvent()
,而且起默认都会消耗掉事件。除非它是不可点击的(即clickable
和longClickable
均为false
)。View 的longClickable
默认都为false
,而clickable
分状况,好比Button
默认为true
,TextView
默认为false
。- View 的
enable
属性不会影响onTouchEvent()
的默认返回值,哪怕一个View
是disable
状态的,只要它的clickable
或者longClickable
有一个为true
,那么它的onTouchEvent()
就会返回true
。requestDisallowInterceptTouchEvent()
能够在子元素中干预父元素的事件分发过程,可是没法干预 ACTION_DOWN 事件。- 事件优先顺序:
setOnTouchListener()
=>onTouchEvent()
=>onClickListener()
一不当心发现仍是挺多的,固然这些都是结论,具体能够跟着 面试系列:讲讲 Android 的事件分发机制 进行源码流程探讨,你会发现上面的结论很容易获得。开发
对于大多数 Android 开发来讲,处理滑动冲突好像很难,但实战一下又发现,好像也挺简单,由于这个其实是有套路可循的。基本就两种方案:外部拦截法 && 内部拦截法。get
所谓外部拦截法,顾名思义,就是直接在父容器中直接拦截掉咱们的滑动事件,让其不能进入到子元素中,这彷佛和咱们 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_DOWN
和 ACTION_UP
,想必你们也清楚了。上面说了,若是拦截了 ACTION_DOWN
事件,那后续的 ACTION_MOVE
、ACTION_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 哟。
那么今天的分享就到这里啦
喜欢文章的小伙伴别忘了点个关注,留个赞再走呀,一个专一Android面试的小喵喵~