5-事件分发机制
事件分发
当点击事件发生时,事件最先传递给 Activity,Activity 会首先将事件将被所属的 Window(PhoneWinodw)进行处理,即调用 superDispatchTouchEvent () 方法。通过观察 superDispatchTouchEvent ()方法的调用链,我们可以发现事件的传递顺序:
- PhoneWinodw.SuperDispatchTouchEvent ()
- DecorView(FrameLayout). DispatchTouchEvent (event)
- ViewGroup.DispatchTouchEvent (event)
- 事件一层层传递到了 ViewGroup 里。
- 每到一个子 view,看他的 onInterceptTouchEvent 方法是否拦截,ontouch 是否消费方法,如果没有继续向下 dispatchTouchEvent 分发事件,都不处理向上传(事件流向),都不处理就会回溯到 activity,若顶层(activity)也不对此事件进行处理,此事件相当于消失了(无效果)。
- return true 代表终止事件传递,return false 则是回溯到父 View 的 onTouchEvent 方法。
- View没有 onInterceptTouchEvent ()方法,有 dispatchTouchEvent,一但有点击事件传递给它,它的 ouTouchEvent ()方法就会被调用。
- ouTouchEvent 是否消费事件取决于 ACTION_DOWN 事件或 POINT_DOWN 事件是否返回为
true
- 只有前一个事件(如 ACTION_DOWN)返回 true,才会收到 ACTION_MOVE 和 ACTION_UP 的事件。
事件冲突
不同向嵌套
- 外部处理,重写父 view 的 onInterceptTouchEvent ,MotionEvent 的事件全部返回 false,不拦截;
- 内部处理。重写子 view 的 dispatchTouchEvent,通过 requestDisallowInterceptTouchEvent 方法(这个方法可以在子元素中干预父元素的事件分发过程),请求父控件不拦截自己的事件,true 是不拦截,false 是拦截。
1 |
|
同向嵌套
父 View 会彻底卡住子 View
原因:抢夺条件一致,但父 View 的 onInterceptTouchEvent () 早于子 View 的 dispatchTouchEvent ()
本质上是策略问题:嵌套状态下用户手指滑动,他是想滑谁?
解决⽅方案:
实现策略—父 View、子 View 谁来消费事件可以实时协商
- 换成 NestedScrollView:可以滑动
- 实现 NestedScrollingChild 3 接口来实现自定义的嵌套滑动逻辑
[[NestedScrollingParent]]
自定义 ViewGroup 的触摸反馈
- 除了重写 onTouchEvent () ,还需要重写 onInterceptTouchEvent ()
- onInterceptTouchEvent () 不用在第一时间返回 true,而是在任意事件,需要拦截的时候返回 true 就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//伪代码
view.dispatchTouchEvent();
public boolean dispatchTouchEvent(MotionEvent event) {
return ontouchEvent();
}
ViewGroup.dispatchTouchEvent();
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result;
if (interceptTouchEvent()) {
result = ontouchEvent();
} else {
result = 子 view的 dispatchTouchEvent ();
}
return result;
}
Dialog 是 Focusable 的。如果不是 Focusable 的,那么 Dialog 弹出后 Activity 还是可以响应用户事件的
ViewGroup 类中,实际是没有 onTouchEvent 方法的,但是由于 ViewGroup 继承自 View,而 View 拥有 onTouchEvent 方法,故 ViewGroup 的对象也是可以调用 onTouchEvent 方法的。故在表格中表明 ViewGroup 中存在 onTouchEvent 方法的。
递归
ViewGroup (View). DispatchTouchEvent ()
- ViewGroup.OnInterceptTouchEvent ()
- child.DispatchTouchEvent ()
- View.OnTouchEvent ()
Activity 的分发方法中调用了 onUserInteraction法,你能说说这个方法有什么作用吗?
这个方法在 Activity 接收到 down 的时候会被调用,本身是个空方法,需要开发者自己去重写。
这个方法会在我们以任意的方式开始与 Activity 进行交互的时候被调用。
比较常见的场景就是屏保: 当我们段时间没有操作会显示一张图片,当我们开始与 Activity 交互的时候可在这个方法中取消屏保: 另外还有没有操作自动隐藏工具栏,可以在这个方法中让工具栏重新显示。
一个场景
有一个 ViewGroup, 然后手指头接触 Button , 手指头滑开了, 滑开又松手的过程, 整个事件发生了什么? 经历了什么?
一开始 ViewGroup 会接受到整个事件序列的第一个事件: ACTION_DOWN,ViewGroup dispatchTouchEvent 收到 ACTION_DOWN 后,
开始询问 ViewGroup onInterceptTouchEvent 是否需要拦截,
默认情况下 ViewGroup onInterceptTouchEvent 返回 false 不拦截,开始向下传递 ACTION_DOWN 事件,
Buttton dispatchTouchEvent 收到 ACTION_DOWN 询问 onTouchEvent 是否处理,
Button 默认处理,此后的所有事件序列都直接跨过 ViewGroup onInterceptTouchEvent 的判断直接传递给 Button,
但 ViewGroup dispatchTouchEvent 会收到所有事件。随着手指的滑动 Button 的坐标发生了改变,当手指抬起时触发 Button onClick
事件。(移动出自己的范围,就消失了)
点击 button,recycleview 中滑动,滑动会收到 cancel 事件
自定义单 View 的触摸反馈
View.OnTouchEvent ()
如果不在滑动控件中,切换至按下状态,并注册长按计时器
重绘 Ripple Effect
如果移动出自己的范围,自我标记本次事件失效,忽略后续事件
如果是按下状态并且未触发长按,切换至抬起状态并触发点击事件,并清除⼀切状态
如果已经触发长按,切换至抬起状态并清除一切状态
切换至抬起状态,并清除一切状态
View.DispatchTouchEvent ()
View 中 setOnTouchListener 的 onTouch,onTouchEvent,onClick 的执行顺序
View 的 dispatchTouchEvent 源码
1 |
|
当以下三个条件任意一个不成立时
- mOnTouchListener 不为 null
- View 是 enable 的状态
- mOnTouchListener.OnTouch (this, event)返回 true
函数会执行到 onTouchEvent。在这里我们可以看到,首先执行的是 mOnTouchListener. OnTouch 的方法,然后是 onTouchEvent 方法继续追溯源码,到 onTouchEvent ()观察,发现在处理 ACTION_UP 事件里有这么一段代码
1 |
|
此时可知,onClick 方法也在最后得到了执行所以三者的顺序是:
- OnTouchListener () 的 onTouch
- OnTouchEvent ()
- OnClick ()
首先,他会判断是否存在 onTouchListener,存在则会调用他的 onTouch 方法来处理事件。如果该方法返回 true 那么就分发结束直接返回。
而如果该监听器为 null 或者 onTouch 方法返回了 false,则会调用 onTouchEvent 方法来处理事件
OnTouchEvent 方法中支持了两种监听器: onClickListener 和 onLongClickListener。 View 会根据不同的触摸情况来调用这两个监听器。同时进入到 onTouchEvent 方法中,无论该 view 是否是 enable,只要是 clickable,他的分发方法都是返回 true。
总结一下就是: 先调用 onTouchListener,再调用 onClickListener 利 onlongClicklistener.
OnTouch 利 onTouchEvent 区别,调用顺序?
在 Android 开发中,onTouch
和 onTouchEvent
都是用来处理触摸事件的方法,但是它们之间有一些区别:
onTouch
方法是 View 类的一个回调方法,用于处理触摸事件。它通常用于实现对触摸事件的自定义处理逻辑。onTouch
方法需要返回一个布尔值,表示是否消费了该触摸事件。onTouchEvent
方法同样是 View 类的一个回调方法,也用于处理触摸事件。与onTouch
方法不同的是,onTouchEvent
方法是一个事件处理器,用于处理特定类型的触摸事件。在onTouchEvent
方法中,你可以根据不同的动作类型(如按下、移动、抬起等)来处理触摸事件。
调用顺序通常是这样的:
- 当一个触摸事件发生时,首先会调用
dispatchTouchEvent
方法,该方法会将触摸事件分发给相应的 View。 - 如果一个 View 的
onTouch
方法被调用,那么它会处理该触摸事件,并返回一个布尔值,表示是否消费了该事件。如果onTouch
方法返回 true,表示事件被消费;如果返回 false,会继续向下传递。 - 如果事件未被
onTouch
方法消费,那么会调用该 View 的onTouchEvent
方法进行处理。
总结起来,onTouch
方法和 onTouchEvent
方法都是用于处理触摸事件的,但是 onTouch
方法更加灵活,可以自定义处理逻辑并决定是否消费事件,而 onTouchEvent
方法则是针对具体的触摸事件类型进行处理的事件处理器。
一般什么时候需要 onTouch 返回 true
- 拦截点击事件:如果你想拦截点击事件,阻止它向下传递给其他视图或控件,可以在
onTouch
方法中返回true
。 - 处理滑动事件:当你需要在自定义 View 中实现滑动操作时,你可能会在
onTouch
方法中捕获滑动手势并返回true
,以表示你已经处理了该滑动操作。 - 自定义触摸交互:如果你希望在自定义 View 中实现特定的触摸交互,例如绘制、缩放、旋转等,你可能需要在
onTouch
方法中返回true
以消费触摸事件。
总的来说,当你希望在自定义 View 中控制触摸事件的传递和处理逻辑时,你可能需要在 onTouch
方法中返回 true
。这样可以确保触摸事件被当前视图消费,并阻止其传递给其他视图或控件。
view 的事件分发:View 为啥会有 dispatchTouchEvent 方法?
View 可以注册很多事件监听器,事件的调度顺序是 onTouch> onTouchEvent>onLongClickListener> onClickListener
判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;
在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;
最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。
然后介绍了整个事件分发中的几个特殊的点。
DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;
MFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构
事件分发的流程
事件分发是针对一次手势的过程,这个手势包含一次 ACTION_DOWN,多次 ACTION_MOVE,和一次 ACTION_UP”,在 ACTION_DOWN 的时候来决定本次事件的“TargetView”,该 View 会决定这次事件分发的事件流向。父控件可以在 ACTION_DOWN 或者 targetView 不为空的情况下,进行拦截,如果拦截了 targetView 的事件,会给它发一个 ACTION_CANCEL。
CANCEL 事件的触发场景
当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。
TouchTarget
作用:记录每个子 View 是被哪些 pointer(手指)按下的
结构:单向链表
一次事件分发过程中会有多次 ACTION_POINT_DOWN 吗
会。多指的时候,你按下第一个手指,再按下第二个手指 ACTION_POINTER_DOWN 就来了。你可以看看 ViewPager,ScrollView 这些官方类源码,都有多指的处理。
一个 LinearLayout 内部有两个 Button,当我第一个手指按在第一个 Button 上,第二个手指按在第二个 Button 上,这两个 Button 哪个会收到 ACTION_DOWN?分别抬起每个手指,哪个 Button 的 Click 会被触发?
第一个手指按下第一个 Button 上,第一个 Button 收到 ACTION_DOWN。第二个手指按在第二个 Button 上,第二个 Button 竟然也收到了 ACTION_DOWN。分别抬起时,两个 Button 的 Click 依次触发。
当第二根手指去按下 Button 2 之后,确实会在 Button 2 中收到 ACTION_ DOWN 事件, 这样加上 Button 1 按下时收到的 ACTION_ DOWN, - -共就两次了,这两次 ACTION_ DOWN 都是在同- -次事件分发中发生的。
但是,对于一个 View 自身来说,同一次事件分发中是不会收到两次 ACTION_ DOWN 的(手动重写 dispatchTouchEvent 并在里面改了Action 的不算)。
一次事件分发过程中,可能会有多次 ACTION_DOWN 吗?
没有,只有你第一个手指按下才是 ACTION_DOWN,从第二个开始都是 ACTION_POINTER_DOWN,当然了前提是你自己不修改 dispatchTouchEvent 分发逻辑哈。
ViewGroup.DispatchTouchEvent ()
- 如果是用户初次按下(ACTION_DOWN),清空 TouchTargets 和 DISALLOW_INTERCEPT 标记
- 拦截处理
- 如果不拦截并且不是 CANCEL 事件,并且是 DOWN 或者 POINTER_DOWN,尝试把 pointer(手指)通过 TouchTarget 分配给子 View;并且如果分配给了新的子 View,调用 child.DispatchTouchEvent () 把事件传给子 View
- 如果没有,调用⾃己的 super.DispatchTouchEvent ()
- 如果有,调用 child.DispatchTouchEvent () 把事件传给对应的子 View(如果有的话)
- 如果是 POINTER_UP,从 TouchTargets 中清除 POINTER 信息;如果是 UP 或 CANCEL,重置状态
ViewGroup 第一步会判读这个事件是否需要分发给了 view,如果是则调用 onInterceptTouchEvent 方法判断是否要进行拦截。
第二步是如果这个事件是 down 事件,那么需要为他寻找一个消费此事件的子控件,如果找到则为他创建一个 TouchTarget。
第三步是派发事件,如果存在 TouchTarget,说明找到了消费事件序列的子 view,直接分发给他。如果没有则交给自己处理
onInterceptTouchEvent返回false, dispatchTouchEvent返回true会发生什么
当 onInterceptTouchEvent
方法返回 false
,而 dispatchTouchEvent
方法返回 true
时,触摸事件会继续传递给子视图进行处理,并且该 ViewGroup 不会拦截触摸事件。许触摸事件在视图层次结构中向下传递,直到找到一个视图来处理该事件。
具体来说,当一个触摸事件到达一个 ViewGroup 时,首先会调用该 ViewGroup 的 onInterceptTouchEvent
方法来判断是否要拦截该触摸事件。如果 onInterceptTouchEvent
返回 false
,表示该 ViewGroup 不拦截触摸事件,将会继续传递给子视图。
接着,如果 dispatchTouchEvent
方法返回 true
,则表示该 ViewGroup 处理了触摸事件,并且将事件传递给子视图进行进一步处理。子视图的 dispatchTouchEvent
方法会被调用,触摸事件会在子视图间进行传递,直到找到一个视图处理该事件或事件被消费。
如果都是false
事件会继续向上传递给它的父视图,以便父视图处理该事件。 因此,在 onInterceptTouchEvent 返回 false 且 dispatchTouchEvent 返回 false 的情况下,触摸事件会继续向上层视图传递,直到找到一个视图来处理该事件。如果没有找到可以处理该事件的视图,那么事件可能会被传递给包含该视图的 Activity 或窗口进行处理,或者被丢弃。最终的处理结果取决于视图层次结构中的其他视图和应用程序的逻辑
自定义 viewpager 的 onInterceptTouchEvent
OnTouchEvent
一般自定义 onTouchEvent 方法流程
- 在 down 的时候去记录坐标点
GetX/getY 获取相对于当前 View 左上角的坐标,getRawX/getRawY 获取相对于屏幕左上角的坐标。比如接触到按钮时,x,y 是相对于该按钮左上点的相对位置。而 rawx, rawy 始终是相对于屏幕的位置。 - Move 的时候计算偏移量,并用 scrollTo ()或 scrollBy ()方法移动 view。这俩个方法都是快速滑动,是瞬间移动的。注意:滚动的并不是 viewgroup 内容本身,而是它的矩形边框。
- 在 up 的时候,判断应显示的页面位置,并计算距离、滑动页面。