文章

事件分发面试题

事件分发面试题

事件分发基础

事件分发

事件基础

Window
IMS(InputManagerService)
ViewRootImpl
每一颗树有一个根,就是 ViewRootImpl,管理整颗树的绘制、事件分发等。

硬件层级事件分发

  1. 在 system_server 进程中,启动了 IMS,WMS 等服务
  2. ViewRootImpl 的创建
    1. ActivityThread 创建
    2. 启动一个 Activity
    3. 通过 WindowManager,和 WMS 通信 addView,这个过程会创建 ViewRootImpl
    4. ViewRootImpl 会创建 InputChannel、InputQueue,创建 WindowInputEventReceiver,接收输入事件
  3. 屏幕捕获到触摸事件,组装成 MotionEvent 对象,交给 IMS(InputManagerService),IMS 是在 system_server 启动的时候加载的,是一个系统服务;system_server 还启动了 AMS/PMS/WMS 等几十种系统服务
  4. IMS 通过 WMS 找到激活的 window,将触摸事件交给了 ViewRootImpl

image.png

  1. ViewRootImpl 通过一条 InputStage 链来分发各种事件,触摸事件在ViewPostImeInputStage处理,不管事件是否消费,所有的 InputStage 都会被调用判断顶层的 view 非 DecorView 是其他的 ViewGroup,那么正常事件分发处理
  2. ViewRootImpl 判断顶层 view 时候是 DecorView,是 decorView,调用 Window 的 Callback,这个 Callback 就是 Activity
  3. 事件就到了 Activity 处理事件的入口
  4. 再分发到 DecorView,下面就是 UI 层级事件分发了

image.png

UI 层级事件分发

View 的分发

  1. 处理 onTouch 监听
  2. View 在 TouchEvent 处理事件的点击长按

ViewGroup 的分发

  1. 拦截事件:在一定情况下,viewGroup 有权利选择拦截事件或者交给子 view 处理
  2. 寻找接收事件序列的控件:每一个需要分发给子 view 的 down 事件都会先寻找是否有适合的子 view,让子 view 来消费整个事件序列,找一个 TouchTarget
  3. 派发事件:把事件分发到感兴趣的子 view 中或自己处理

image.png
以方法为核心总结:
image.png

事件冲突解决的方式?

外部拦截法

触摸事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截。
父容器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:

  1. 在子 View 处理事件的过程中,父 View 对事件拦截。

父容器在 DOWN 事件没有拦截且子 view 消费了事件,但在 MOVE 事件拦截了,此时子 View 会收到 Cancel 事件

  1. ACTION_DOWN 初始化操作。
  2. 在子 View 处理事件的过程中被从父 View 中移除时。
  3. 子 View 被设置了 PFLAG_CANCEL_NEXT_UP_EVENT 标记时。
  4. 如果触摸了某个控件,但是又不是在这个控件的区域上抬起(移动到别处),会出现 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() 和返回值意义?

  1. 首先是 ViewGroup 的 onInterceptTouchEvent(),返回 true,会将事件进行拦截,交给该层 ViewGroup 的 super.dispatchTouchEvent 处理;如果返回 false,那么继续往下传递
  2. 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);
                    }
                } 
            }
        }
    }
}
  1. y 轴滑动距离大于 mTouchSlop 且只要不是垂直的嵌套滑动(嵌套个垂直 rv,就不会拦截),都拦截
  2. 不能滚动时,不拦截事件
  3. 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 都加在头节点

本文由作者按照 CC BY 4.0 进行授权