基于https://www.wanandroid.com每日一问的笔记,做一些整理,方便自己进行查看和记忆。
事件分发原理
- 事件分发,其实就是一个责任链的变种,这个责任链,是一个设计模式。
- 在Android中,当最顶层的View收到事件之后,会一级一级地往下传,在每一级View中,它们各自都有权利去处理(也就是拦截)这个事件,如果这次的事件传到了最底层的View,也没能处理的话,就会从这个最底层的View一级一级地向上传回去。
嵌套滚动
- 这个嵌套滚动,说的应该不是普通的嵌套滚动(比如ScrollView套ListView),而是说NestedScrollingParent和NestedScrollingChild,这两个东西,出来挺久了,可能好多同学还是觉得有点陌生,但我们在很多场景下,已经在不识不觉中使用它了,比如说CoordinatorLayout,它是一个NestedScrollingParent,还有RecycleView,它是一个NestedScrollingChild。
一个最常见的效果:列表向上滚动,ToolBar收起,反之,当列表向下滚动时,ToolBar随着列表的滚动出现。这个效果,用NestedScrolling来实现,可以非常简单。嵌套滚动原理
- 它的原理,很简单:在NestedScrollingChild滚动过程中,它和NestedScrollingParent会一直”保持通讯”,比如:
- 当Child滚动之前,会通知Parent:”我要开始滚动啦,你看你要不要做点什么”。
- 当Child在滚动的时候,也会每次通知Parent:”我这次消费了xxx,你看你还要做什么”。
- 当Child滚动完成,Parent也会收到通知:”我滚动完成了”。
- 除了手指触摸滚动的,还有惯性滚动,但原理和流程是一样的。
至于为什么嵌套滚动有必要存在,我觉得有以下几个原因:
- 减少工作量,比如说一些看似很复杂滚动效果,在使用NestedScrolling机制之后,就变得简单起来了。
- 降低耦合度,在NestedScrolling机制出现之前,很多与子View有滚动交互的ViewGroup,大部分处理滚动的代码,都堆积这个ViewGroup中。而推出了NestedScrolling之后,这个滚动的子View,由被动方,变成了主动方(滚动的状态都是由这个子View去决定,不再需要ViewGroup去主动判断)。
- 增加灵活性,CoordinatorLayout的强大,相信同学们都体会到了,它可以通过设置各种不同的Behavior,来定制它的交互效果。
简单介绍 View 的时间分发机制
当然,这里也可以简单地提一下,基本的流程就是下面的伪代码。
123456789public 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,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶层 DocorView,然后遵循上面的方式一直在最里层 View。
- 而处理事件则从最里层 View 不断回传给自己的外层 View,如果一直没有 View 进行处理,则直接会回传到 Activity 中。1onTouchEvent() 返回 true 代表自己要处理。
既然都提了这么一点,也就突然想给出一些结论,参考自 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()
处理自定义 View 中的滑动冲突
— 对于大多数 Android 开发来说,处理滑动冲突好像很难,但实战一下又发现,好像也挺简单,因为这个实际上是有套路可循的。基本就两种方案:外部拦截法 && 内部拦截法外部拦截法
所谓外部拦截法,顾名思义,就是直接在父容器中直接拦截掉我们的滑动事件,让其不能进入到子元素中,这似乎和我们 RecyclerView 嵌套 RecyclerView 时禁用内部 RecyclerView 滑动有那么一丝相似之处,就是内部不处理就完事儿了。但细细品来又完全不一样,这里的外部拦截法会让内部元素根本就收不到滑动事件。
这种方法明显非常适合我们上面讲的事件分发机制。我们在接收 ACTION_MOVE 事件的时候,直接通过使 onInterceptTouchEvent() 方法返回 true 来直接拦截掉事件就可以了,伪代码想必大家也知道了:
12345678override 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() 方法,得到伪代码如下:
12345678910111213override 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() 方法。12345678override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {ev?.run {if (action == MotionEvent.ACTION_DOWN){return false}}return super.onInterceptTouchEvent(ev)}