事件分发面试题
事件分发基础
事件分发
事件基础
Window
IMS(InputManagerService)
ViewRootImpl
每一颗树有一个根,就是 ViewRootImpl,管理整颗树的绘制、事件分发等。
硬件层级事件分发
- 在 system_server 进程中,启动了 IMS,WMS 等服务
- ViewRootImpl 的创建
- ActivityThread 创建
- 启动一个 Activity
- 通过 WindowManager,和 WMS 通信 addView,这个过程会创建 ViewRootImpl
- ViewRootImpl 会创建 InputChannel、InputQueue,创建 WindowInputEventReceiver,接收输入事件
- 屏幕捕获到触摸事件,组装成 MotionEvent 对象,交给 IMS(InputManagerService),IMS 是在 system_server 启动的时候加载的,是一个系统服务;system_server 还启动了 AMS/PMS/WMS 等几十种系统服务
- IMS 通过 WMS 找到激活的 window,将触摸事件交给了 ViewRootImpl
- ViewRootImpl 通过一条 InputStage 链来分发各种事件,触摸事件在ViewPostImeInputStage处理,不管事件是否消费,所有的 InputStage 都会被调用判断顶层的 view 非 DecorView 是其他的 ViewGroup,那么正常事件分发处理
- ViewRootImpl 判断顶层 view 时候是 DecorView,是 decorView,调用 Window 的 Callback,这个 Callback 就是 Activity
- 事件就到了 Activity 处理事件的入口
- 再分发到 DecorView,下面就是 UI 层级事件分发了
UI 层级事件分发
View 的分发
- 处理 onTouch 监听
- View 在 TouchEvent 处理事件的点击长按
ViewGroup 的分发
- 拦截事件:在一定情况下,viewGroup 有权利选择拦截事件或者交给子 view 处理
- 寻找接收事件序列的控件:每一个需要分发给子 view 的 down 事件都会先寻找是否有适合的子 view,让子 view 来消费整个事件序列,找一个 TouchTarget
- 派发事件:把事件分发到感兴趣的子 view 中或自己处理
事件冲突解决的方式?
外部拦截法
触摸事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截。
父容器onInterceptTouchEvent,我想要把事件分发给谁就分发给谁;ACTION_DOWN 不要拦截,如果拦截,那么后续事件就直接交给父 View 处理了,也就没有拦截和不拦截的问题了
内部拦截法
父容器不做任何拦截,而是将所有的事件都传递给子容器,如果子容器需要此事件那就直接消耗,否则就交给父容器进行处理。
子 view 的 requestDisallowInterceptTouchEvent
手指放开的时候,如何实现弹性滑动
Scroller?
1
2
3
4
5
6
7
8
case MotionEvent.ACTION_UP:
/**
* scrollY是指:View的上边缘和View内容的上边缘(其实就是第一个ChildView的上边缘)的距离
* scrollY=上边缘-View内容上边缘,scrollTo/By方法滑动的知识View的内容
* 往下滑动scrollY是负值
*/
int scrollY=getScrollY();
smoothScrollByScroller(scrollY);
收到 CANCEL 事件的几种情况
上层 View 回收事件处理权的时候,childView 才会收到一个 ACTION_CANCEL 事件。
https://mp.weixin.qq.com/s/glkmajbaUMN_4ZAKMpxvGg
有四种情况会触发 ACTION_CANCEL:
- 在子 View 处理事件的过程中,父 View 对事件拦截。
父容器在 DOWN 事件没有拦截且子 view 消费了事件,但在 MOVE 事件拦截了,此时子 View 会收到 Cancel 事件
- ACTION_DOWN 初始化操作。
- 在子 View 处理事件的过程中被从父 View 中移除时。
- 子 View 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT 标记时。
- 如果触摸了某个控件,但是又不是在这个控件的区域上抬起(移动到别处),会出现 ACTION_CANCEL
事件相关面试题
事件从哪里来?
屏幕→输入系统→WMS→View
- 点击屏幕,会记录下 x,y 坐标并导电转换成电频传给传感器,传感器通过电路板把硬件中断事件发给 Linux 操作系统
- 输入系统
- Linux 内核会在/dev/input 中创建对应的设备节点,用户操作这些输入设备会产生各种事件(按键事件、触摸事件、鼠标事件等),输入事件产生的原始信息会被 Linux 内核中的输入子系统采集,原始信息由内核空间的驱动层一直传递到用户空间的设备节点;
- IMS 所做的工作就是监听/dev/input 下的所有的设备节点,当设备节点有数据时会将数据进行加工处理并找到合适的 Window,将输入事件派发给它。
- WMS WMS 职责之一就是输入系统的中转站,WMS 作为 Window 的管理者,会配合 IMS 将输入事件交给合适的 Window 来处理
- 最后事件会最先发给 ViewRootImpl 的 DecorView,然后转发给 Activity,再分发到 PhoneWindow→DecorView→根 ViewGroup
事件一定经过 Activity 吗?
不一定。只有绑定在 Activity 的 PhoneWindow,事件才会经过 Activity;像 Dialog,PopupWindow 不会经过 Activity
Activity 的分发方法中调用了 onUserInteraction() 方法,你能说说这个方法有什么作用吗?
Activity 接收到 down 的时候会被调用,这个方法会在我们以任意的方式开始与 Activity 进行交互的时候被调用。比较常见的场景就是屏保:当我们一段时间没有操作会显示一张图片,当我们开始与 Activity 交互的时候可在这个方法中取消屏保;另外还有没有操作自动隐藏工具栏,可以在这个方法中让工具栏重新显示。
ViewGroup 在 down 事件拦截的处理?以及 down 不拦截,在 move,up 事件时拦截后的表现?
- 在 down 事件中拦截,那么会调用该 ViewGroup 的 super.dispatchTouchEvent() 方法(也就是 View 的 dispatchTouchEvent() 方法);
- 如果 down 事件没有拦截,但是后续的 move 或 up 事件进行了拦截,那么在拦截的那次 move 事件或 up 事件,会传递cancel事件,并 return true;在后续第二次后的 move 或者 up 事件,会调用该 ViewGroup 的 super.dispatchTouchEvent() 方法(也就是 View 的 dispatchTouchEvent() 方法);
一个 View/ViewGroup,down 事件到来未消费处理,后续的 move 和 up 事件还会来吗?
如果 DOWN 事件未消费,后续的 MOVE/UP 事件都不会有。
这是因为 down 事件未消费的话,那么 mFirstTouchTarget=null 即没有子 view 能消费事件;在后续 move 事件到来时,会默认拦截事件,这个事件就直接交给了父容器自身 View#dispatchTouchEvent/onTouchEvent 处理了。
子 view 消费了事件后,后续的 move/up 事件都是只交给子 view?父容器的 dispatchTouchEvent 不会收到事件?
父容器的 dispatchTouchEvent 能收到事件,事件是从父到子一路下来,一路上的 ViewGroup 都是能收到事件的。
如果有子 View 消费了事件后 (mFirstTouchTarget!=null),父容器的 dispatchTouchEvent 就不会遍历子 view 分发事件了,直接分发给这个消费事件的子 View 了。
DecorView 什么时候生成?
setContentView 或者 handlerResume
Ref
事件相关问题
事件基础
事件一定经过 Activity 吗?
不一定。只有绑定在 Activity 的 PhoneWindow,事件才会经过 Activity;像 Dialog,PopupWindow 不会经过 Activity
衍生问题: 事件分发,真的一定从 Activity 开始吗?
不是,ViewRootImpl/Window 讲起
事件分发由谁负责?
Window ViewRootImpl
Activity 的分发方法中调用了 onUserInteraction() 方法,你能说说这个方法有什么作用吗?
这个方法在 Activity 接收到 down 的时候会被调用,本身是个空方法,需要开发者自己去重写。
通过官方的注释可以知道,这个方法会在我们以任意的方式开始与 Activity 进行交互的时候被调用。比较常见的场景就是屏保:当我们一段时间没有操作会显示一张图片,当我们开始与 Activity 交互的时候可在这个方法中取消屏保;另外还有没有操作自动隐藏工具栏,可以在这个方法中让工具栏重新显示。
和 onUserLeaveHint
用来通知用户交互了,来操作管理状态栏通知。
MotionEvent/KeyEvent 何时包装的?
APP 进程是如何和 IMS 通信的?
InputChannel(Socket?)
基本事件问题
onTouch、onClick、onLongClick、onTouchEvent
onTouch 和 onTouchEvent 有什么区别及屏蔽 onTouchEvent?
在 View 进行 dispatchTouchEvent 的时候,会先进行 onTouch,根据 onTouch 的返回值;如果为 true,那么不会再执行 onTouchEvent,如果为 false,那么会执行 onTouchEvent。
关键源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (onFilterTouchEventForSecurity(event)) {
// ...
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
}
onClick、onTouch 和 onLongClick 区别和执行顺序及如何屏蔽 onClick 或 onLongClick?
- 先执行 onTouch,根据 onTouch 的返回值;如果为 true,那么 onClick 和 onLongClick 都不会执行了;
- onTouch 如果返回 false,那么会在 DOWN 事件,进行 longClick 事件的判断,如果在 500ms 有 UP 事件,那么不算长按事件,响应单击事件;如果在 500ms 内没有 UP 事件,那么会响应 onLongClick,根据 onLongClick 的返回值,为 true 不不再响应 onClick,为 false 接着响应 onClick
1
2
1. 小于500ms,onclick执行,onlongclick不执行
2. 大于500ms,执行onlongclick,返回值true不执行onclick,false执行onclick
事件的消费和调用 onClick、onLongClick 是两码事?
事件的消费,指 View 的 dispatchTouchEvent() 返回了 true,包括 onTouch 返回 true、onTouchEvent 返回 true,
而 onTouchEvent 返回 true 包括 View 可 clickable 或 longclickable(不管该 view 是否 enable);
而如果 View 调用了 onClick 或 onLongClick 那么该事件一定被消费了;
但是消费了事件,不代表 onClick 或 onLongClick 会调用,比如 View 当前处于 disable 状态
一个 View 设置为 disable 了,但设置了 onClick 或 onLongClick,会消费事件吗?onClick 或 onLongClick 会执行吗?
会消费事件,但是不会执行 onClick 或 onLongClick。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean onTouchEvent(MotionEvent event) {
// ...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
// ...
}
ViewGroup 在 down 事件拦截的处理?以及 down 不拦截,在 move,up 事件时拦截后的表现?
在 down 事件中拦截,那么会调用该 ViewGroup 的 super.dispatchTouchEvent() 方法(也就是 View 的 dispatchTouchEvent() 方法);
如果 down 事件没有拦截,但是后续的 move 或 up 事件进行了拦截,那么在拦截的那次 move 事件或 up 事件,会传递 cancel 事件,并 return true;在后续第二次后的 move 或者 up 事件,会调用该 ViewGroup 的 super.dispatchTouchEvent() 方法(也就是 View 的 dispatchTouchEvent() 方法);
View 的 dispatchTouchEvent()、onTouch()、onTouchEvent() 的作用?
dispatchTouchEvent() 的作用其实就是为了 onTouch() 的监听;
onTouch() 就是对 onTouchEvent() 的一个屏蔽和扩展的作用;
onTouchEvent() 就是为了 onClick()、onLongClick() 的监听。
ViewGroup 的 onInterceptTouchEvent()、dispatchTouchEvent() 及 View 的 dispatchTouchEvent()、onTouchEvent() 和返回值意义?
- 首先是 ViewGroup 的 onInterceptTouchEvent(),返回 true,会将事件进行拦截,交给该层 ViewGroup 的 super.dispatchTouchEvent 处理;如果返回 false,那么继续往下传递
- View 的 onTouchEvent 返回 true,表明该 View 可以消费该事件,那么后续的事件都交给该 View 的 onTouchEvent 进行处理。
一个 View/ViewGroup,down 事件到来未消费处理,后续的 move 和 up 事件还会来吗?
如果 DOWN 事件未消费,后续的 MOVE/UP 事件都不会有
这是因为 down 事件未消费的话,那么 mFirstTouchTarget=null 即没有子 view 能消费事件;在后续 move 事件到来时,会默认拦截事件,这个事件就直接交给了父容器自身 View#dispatchTouchEvent/onTouchEvent 处理了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // 不是DOWN事件且mFirstTouchTarget==null(没有消费事件的view)intercepted==true
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (!canceled && !intercepted) {
// ...
}
if (mFirstTouchTarget == null) { // 如果Down事件未消费,这里为null;后续move事件也会走到这里,调用View#dispatchTouchEvent,就不会分发给子类了
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// ...
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); // 后续事件不会来了
}
return handled;
}
如果你是在根布局是个容器,但是没有任何能消费事件的 view,当 down 事件(未消费事件)过后,那么后续 move/up 事件都不会到来,且 dispatchTouchEvent 也不会调用?Activity 的 dispatchTouchEvent 不会调用?
根布局容器的 dispatchTouchEvent 不会调用, Activity 的 dispatchTouchEvent 会调用。这是因为 Activity 的 dispatchTouchEvent 最终是调用的 DecorView 的 dispatchTouchEvent,如果没有处理,最终调用的是 DecorView 的 onTouchEvent 自己处理,所以自己写的布局的容器的 dispatchTouchEvent 是不会调用的。
down 事件能不能被拦截?可以
如果在父容器 dispatchTouchEvent down 事件时就 intecept 了,那么子 View 是收不到任何事件的。
如果在父容器 dispatchTouchEvent down 事件没有 intecept 了,而是 move 事件才拦截,那么子 View 是可以收到 down 事件并处理,只是后续的 move/up 事件是收不到的,会收到 cancel 事件
所有控件的 down 事件都未消费,事件最终到哪去了?
如果所有控件的事件都未消费,事件会一层层往上传递,最终达到 DecorView,DecorView.super.dispatchTouchEventy,调用 DecorView#onTouchEvent。
1
2
3
4
5
6
7
8
9
10
11
12
13
// DecorView#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
现在看 Activity#dispatchTouchEvent
1
2
3
4
5
6
7
8
9
10
// Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
子 view 消费了事件后,后续的 move/up 事件都是只交给子 view?父容器的 dispatchTouchEvent 不会收到事件?
父容器的 dispatchTouchEvent 能收到事件,事件是从父到子一路下来,一路上的 ViewGroup 都是能收到事件的。
如果有子 View 消费了事件后 (mFirstTouchTarget!=null),父容器的 dispatchTouchEvent 就不会遍历子 view 分发事件了,直接分发给这个消费事件的子 View 了。
什么时候会有 CANCEL 事件?会收到几次 Cancel 事件?
ACTION_CANCEL 的触发条件是事件被上层拦截,到当事件被上层 View 拦截的时候,ChildView 是收不到任何事件的,ChildView 收不到任何事件,自然也不会收到 ACTION_CANCEL 了,所以说这个 ACTION_CANCEL 的正确触发条件并不是这样,那么是什么呢?
事实上,只有上层 View 回收事件处理权的时候,ChildView 才会收到一个 ACTION_CANCEL 事件。
父容器在 DOWN 事件没有拦截且子 view 消费了事件,但在 MOVE 事件拦截了,此时子 View 会收到 Cancel 事件
关键源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) { // 子view收到cancel事件
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// ...
}
只会收到一次 cancel 事件,把当前这个 cancel 事件的 view 从头结点移除,并丢到 TouchTarget 回收池去了,mFirstTouchTarget 指向链表下一个 TouchTarget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if (mFirstTouchTarget == null) {
// ...
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) { // cancel后,mFirstTouchTarget
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
Scrollview 内嵌一个 Button 他的事件消费是怎样的?Move,Down,Up 分别被哪个组件消费?
Button 消费了几个 Move 事件后,接下来的 Move 都被 Scrollview 消费了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
I: [LogScrollView ------>>>>>>onInterceptTouchEvent-false] 【down(action:0,index:0)】
W: [LogButton ---------->>>>>>>>>onTouchEvent-true] 【down(action:0,index:0)】
D: [LogButton -->>dispatchTouchEvent-true] 【down(action:0,index:0)】
D: [LogScrollView -->>dispatchTouchEvent-true] 【down(action:0,index:0)】
V: [LogScrollView ------>>>>>>onInterceptTouchEvent-false] 【move(action:10,index:0)】
V: [LogButton ---------->>>>>>>>>onTouchEvent-true] 【move(action:10,index:0)】
V: [LogButton -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView ------>>>>>>onInterceptTouchEvent-true] 【move(action:10,index:0)】
W: [LogButton ---------->>>>>>>>>onTouchEvent-true] 【cancel(action:11,index:0)】
D: [LogButton -->>dispatchTouchEvent-true] 【cancel(action:11,index:0)】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView ---------->>>>>>>>>onTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView ---------->>>>>>>>>onTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView ---------->>>>>>>>>onTouchEvent-true] 【move(action:10,index:0)】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
在 Scrollview 的源代码里,可以看到 onInterceptTouchEvent 方法中,当判断到开始拖动 Move 事件就被 Scrollview 消费,不再分发给子 View。也就可以解释为什么 Button 消费了几个 Move 之后被父 View 取消。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ScrollView Android-29
/**
* True if the user is currently dragging this ScrollView around. This is
* not the same as 'is being flinged', which can be checked by
* mScroller.isFinished() (flinging begins when the user lifts his finger).
*/
@UnsupportedAppUsage
private boolean mIsBeingDragged = false;
public class ScrollView extends FrameLayout {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { // Move且mIsBeingDragged=true时,拦截事件
return true;
}
if (super.onInterceptTouchEvent(ev)) {
return true;
}
if (getScrollY() == 0 && !canScrollVertically(1)) { // 不能滚动时,不拦截事件
return false;
}
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_MOVE: {
final int yDiff = Math.abs(y - mLastMotionY);
if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { // 只要不是垂直的嵌套滑动,都拦截(如垂直的rv不会拦截)
mIsBeingDragged = true;
mLastMotionY = y;
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
mNestedYOffset = 0;
if (mScrollStrictSpan == null) {
mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
}
final ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
}
}
}
}
- y 轴滑动距离大于 mTouchSlop 且只要不是垂直的嵌套滑动(嵌套个垂直 rv,就不会拦截),都拦截
- 不能滚动时,不拦截事件
- move 事件且 mIsBeingDragged=true 时,拦截事件
TouchTarget 相关问题
TouchTarget 用来做什么的?什么结构?最多保存多少个 TouchTarget?
TouchTarget 用来保存多指触摸的 view 和触摸点;一个 TouchTarget 代表一个消费事件的 View 和其触摸点
- 单链表结构
1
2
3
4
private static final class TouchTarget {
// The next target in the target list.
public TouchTarget next;
}
- 最多保存 32 个 TouchTarget
1
2
3
private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
}
一个 view 最多多少个触摸点?触摸点什么时候更新?怎么更新的 pointerIdBits?
- 一个 view 最多 32 个触摸点,因为保存触摸点的是个 int
1
2
3
4
private static final class TouchTarget {
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
}
- 1 左移 actionIndex 位新增 pointerIdBits
1
1 << ev.getPointerId(actionIndex)
- 触摸点在多指触摸同一个 view 时更新
案例:三指先后触摸在同一个 view 上
1
2
3
4
5
6
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【down(action:0,index:0)】【[ViewGroup(LogFragment)]sRecycledCount=8-
--touchTarget(TouchTarget-228297025), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_down(action:101,index:1)】【[ViewGroup(LogFragment)]sRecycledCount=3-
--touchTarget(TouchTarget-228297025), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(11), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_down(action:101,index:2)】【[ViewGroup(LogFragment)]sRecycledCount=3-
--touchTarget(TouchTarget-228297025), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(111), next(null)】
可以看到三指先后触摸在同一个 view 时,TouchTarget 都是同一个,只是更新 pointerIdBits
TouchTarget 什么时候赋值?清除?
addTouchTarget() 的时候获取,就是有不同的子 View 消费的时候获取;clearTouchTargets() 时清除 mFirstTouchTarget
赋值
1
2
3
4
5
6
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget; // 获取一个TouchTarget,放到头节点
mFirstTouchTarget = target; // mFirstTouchTarget赋值
return target;
}
从回收池中 sRecycleBin
中获取 TouchTarget
清除 mFirstTouchTarget
ACTION_DOWN 初始化的时候会回收;dispatchDetachedFromWindow 时回收;UP/CANCEL 会回收。
1
2
3
4
5
6
7
8
9
10
11
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle(); // 回收
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
TouchTarget.next 什么时候有值?
TouchTarget 是个链表结构,新增的的插在头节点,next 要有值,多点触控第二次 DOWN 是触摸到了不同的 view 且消费了
新增和回收的 touchtarget 是放头还是尾
头
案例:先后触摸在 3 个不同的 View 上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【down(action:0,index:0)】【[ViewGroup(LogFragment)]sRecycledCount=8-
--touchTarget(TouchTarget-158697937), child(LogTextView(41937934)-TextView1), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_down(action:101,index:1)】【[ViewGroup(LogFragment)]sRecycledCount=2-
--touchTarget(TouchTarget-37443383), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(10), next(non-null(158697937))
--touchTarget(TouchTarget-158697937), child(LogTextView(41937934)-TextView1), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_down(action:101,index:2)】【[ViewGroup(LogFragment)]sRecycledCount=1-
--touchTarget(TouchTarget-139554534), child(LogTextView(56693924)-TextView2), pointerIdBits=pointerIdBits(100), next(non-null(37443383))
--touchTarget(TouchTarget-37443383), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(10), next(non-null(158697937))
--touchTarget(TouchTarget-158697937), child(LogTextView(41937934)-TextView1), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_up(action:110,index:2)】【[ViewGroup(LogFragment)]sRecycledCount=2-
--touchTarget(TouchTarget-37443383), child(LogTextView(263162934)-TextView3), pointerIdBits=pointerIdBits(10), next(non-null(158697937))
--touchTarget(TouchTarget-158697937), child(LogTextView(41937934)-TextView1), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【pointer_up(action:110,index:1)】【[ViewGroup(LogFragment)]sRecycledCount=3-
--touchTarget(TouchTarget-158697937), child(LogTextView(41937934)-TextView1), pointerIdBits=pointerIdBits(1), next(null)】
D: [LogFrameLayout -->>dispatchTouchEvent-true] 【up(action:1,index:0)】【[ViewGroup(LogFragment)]mFirstTouchTarget=null】
V: [LogScrollView -->>dispatchTouchEvent-true] 【move(action:10,index:0)】
W: [LogScrollView ---------->>>>>>>>>onTouchEvent-true] 【up(action:1,index:0)】
D: [LogScrollView -->>dispatchTouchEvent-true] 【up(action:1,index:0)】
可以看到,先后触摸在 3 个不同的 View 上,每个 view 都生成了一个 TouchTarget,pointerIdBits 就是指定的 1<<actionIndex
得到,每个生成的 TouchTarget 都加在头节点