本文用于记录各种场景下的工作承受、工作突然被父View阻拦、子View在满足于必定条件下自动抛弃工作等等的办法调用状况,以ScrollView和ScrollView嵌套,内层ScollView中包括多个Button,可是Button将不处理任何工作,并在内层ScrollView上滑动为例:

默许的视图结构:DecorView -> ViewGroup -> OutScrollView -> InnerScrollView -> CustomButton

1. 默许下的一次滑动

OutScrollView在该场景下不阻拦任何工作,全交给内层处理,所以将其当作一个ViewGroup即可。

首先是ACTION_DOWN的下发,工作逐层下发,并且在一切经过的ViewGroup中,都会去调用onInterceptTouchEvent来询问是否需求阻拦工作,终究下发到了叶子节点CustomButton,可是CustomButton并不处理这个工作,它dispatchTouchEvent直接回来了false,因而工作又下沉给它的父View:InnerScrollView:

ViewGroup(47454983,), dispatchTouchEvent:ACTION_DOWN
ViewGroup(47454983,), onInterceptTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), dispatchTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), onInterceptTouchEvent:ACTION_DOWN
CustomButton(227238493), dispatchTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), onTouchEvent:ACTION_DOWN,consumed:true

接下来是ACTION_MOVE工作:

ViewGroup( 47454983 ,), dispatchTouchEvent:ACTION_MOVE
ViewGroup( 47454983 ,), onInterceptTouchEvent:ACTION_MOVE
InnerScrollView( 49665076 ,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView( 49665076 ,), onTouchEvent:ACTION_MOVE,consumed: true
InnerScrollView( 49665076 ,), scolledY: 5
// 下一次ACTION_MOVE工作
ViewGroup(47454983,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(49665076,), dispatchTouchEvent:ACTION_MOVE

咱们可以看到,即便一开端承受ACTION_DOWN的是InnerScrollView,它的父ViewGroup,仍是可以「看到」这一次的工作的,这么规划是由于它的父ViewGroup随时可以去阻拦它的工作,只不过在onInterceptTouchEvent中没有去处理这个工作。

InnerScrollView的onTouchEvent回来了true,即它消费了本次工作,并且消费后造成了内部视图的5点滑动量。

而它内部的CustomButton,由于没有接收到ACTION_DOWN工作,它也就没有时机去接收到后续的ACTION_MOVE和UP工作了。

接下便是很多个ACTION_MOVE,直到ACTION_UP的呈现:

InnerScrollView(49665076,), onTouchEvent:ACTION_MOVE,consumed:true
InnerScrollView(49665076,), scolledY:35
ViewGroup( 47454983 ,), dispatchTouchEvent:ACTION_UP
InnerScrollView( 49665076 ,), dispatchTouchEvent:ACTION_UP
InnerScrollView( 49665076 ,), onTouchEvent:ACTION_UP,consumed: true
InnerScrollView( 49665076 ,), scolledY: 35

2. 父OutScrollView自动阻拦

此前咱们在OutScrollView中的dispatchTouchEvent回来了false,所以它就和一个一般的ViewGroup没什么两样,并不会对内部的InnerScrollView造成搅扰。

现在咱们不去修正原有的逻辑,咱们看看它的工作下发:

OutScrollView(47454983,), dispatchTouchEvent:ACTION_DOWN
OutScrollView(47454983,), onInterceptTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), dispatchTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), onInterceptTouchEvent:ACTION_DOWN
CustomButton(227238493), dispatchTouchEvent:ACTION_DOWN
InnerScrollView(49665076,), scolledY:0
# ACTION_MOVE
OutScrollView(47454983,), dispatchTouchEvent:ACTION_MOVE
OutScrollView(47454983,), onInterceptTouchEvent:ACTION_MOVE
InnerScrollView(49665076,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(49665076,), onTouchEvent:ACTION_MOVE,consumed:true
InnerScrollView(49665076,), scolledY:0

可以看到到一个ACTION_MOVE是由内层的InnerScrollView消费的,只不过咱们手指滑动的比较慢,它的视图偏移量为0,言下之意是:尽管InnerScrollView消费了ACTION_MOVE,可是它并没有造成滑动。由于ScrollView内部会计算滑动量:yDiff,只有yDiff > TouchSlop才能算作是一次有效的滑动,否则不视为滑动。

这么做的目的是优化点按的体验,究竟人很难控制手指在屏幕上不产生一点移动完结一次点击。假如移动一个像素都算滑动的话,那Click简直没法用了。

咱们聚焦到真正地,造成滑动的那一次ACTION_MOVE:

InnerScrollView(227238493,), scolledY:0
# ↑上一个ACTION_MOVE
# ↓下一个ACTION_MOVE
CustomViewGroup(151305542), dispatchTouchEvent:ACTION_MOVE
CustomViewGroup(151305542), onInterceptTouchEvent:ACTION_MOVE
OutScrollView(49665076,), dispatchTouchEvent:ACTION_MOVE
OutScrollView(49665076,), onInterceptTouchEvent:ACTION_MOVE
InnerScrollView( 227238493 ,), dispatchTouchEvent:ACTION_CANCEL
InnerScrollView( 227238493 ,), onTouchEvent:ACTION_CANCEL,consumed: true
# ↓下一个ACTION_MOVE
CustomViewGroup(151305542), dispatchTouchEvent:ACTION_MOVE
OutScrollView(49665076,), dispatchTouchEvent:ACTION_MOVE
OutScrollView(49665076,), onTouchEvent:ACTION_MOVE,consumed:true
OutScrollView(49665076,), scolledY:4
OutScrollView(49665076,), dispatchTouchEvent:ACTION_MOVE

首先工作是经过了OutScrollView,可是工作派发到InnerScrollView的时分ACTION就现已变成了****ACTION_CANCEL,更重要的是接下来,OutScrollView又开端消费一个新的ACTION_MOVE(由体系,经过DecorView新派发出来的),此刻摇身一变OutScrollView开端「独吞」这个工作了,它不再将工作交给InnerScrollView了,终究外部的OutScrollView被成功滑动了,全体产生了4点的偏移量。

促进OutScrollView阻拦工作的的ACTION_MOVE,经过setAction,摇身一变变成了ACTION_CANCEL,可是event仍是那个event,并且当时工作OutScrollView并没有由于阻拦了工作就当即滑动,而是比及下一次的ACTION_MOVE才滑动。

2.2 调试

咱们想要调试这个过程的话,咱们需求在InnerScrollView的onTouchEvent上打上断点,并新增断点条件:

[Android]触摸、滑动与嵌套滑动(二)几种场景下的事件处理分析和调试

由于是源码调试,咱们需求选择对应的源码和对应的模拟器版别:比方现在选中的是Android APi32,那么咱们的源码和模拟器都应该选择32的,否则Debug进入体系代码的时分,比方View类中的代码,行号会和实践的代码行号对不上,第三方品牌的真机大多数也不太靠谱,即便对应Api版别,也很或许行号对应不上,除非用Pixel或许nexus等等原版未经过修正的体系。

调查函数的调用栈,咱们可以定位到OutScrollView的dispatchTransformedTouchEvent中,大致的内容如下:

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;
}

重点可以重视一下**event.setAction(MotionEvent.ACTION_CANCEL);** 这个便是把原先的ACTION_MOVE更改为ACTION_CANCEL的办法。假如child不为空,则把这个撤销工作派发给它。

child是什么?

[Android]触摸、滑动与嵌套滑动(二)几种场景下的事件处理分析和调试

child终究指向的是ViewGroup下的一个mFirstTouchTarget目标,从称号中,咱们大致就可以猜出来它的效果:本ViewGroup第一次接触的目标。

mFirstTouchTarget在各个组件呼应ACTION_DOWN时刻的时分,全部为NULL,直到到第一个组件接收ACTION_DOWN,在这儿是InnerScrollView,咱们看看一切控件的mFirstTouchTarget变量对应的child都是什么:

DecorView -> LinearLayout@892abf
LinearLayout@892abf -> FrameLayout
FrameLayout -> ActionBarOveraLayout
ActionBarOveraLayout -> ContentFrameLayout
ContentFrameLayout -> ConstraintLayout // 这个ConstraintLayout便是根布局
ConstraintLayout -> CustomViewGroup
CustomViewGroup -> OutScrollView
OutSrcollView -> LinearLayoutCompat@20889
LinearLayoutCompat@20889 -> InnerScrollView
InnerScrollView -> null

一切的ViewGroup,终究的mFirstTouchTarget都指向了接收接触工作的那一个控件,终究这个链条会走向消费工作的View,也便是InnerScrollView。

可是一旦OutScrollView下发了ACTION_CANCEL之后,将滑动工作的消费权从InnerScrollView转移到自己身上的时分产生了什么呢?

当然是把这个链条切断了,所以它就成了终究的mFirstTouchTarget,工作也就都由它来消费。

CustomViewGroup -> OutScrollView
OutSrcollView -> LinearLayoutCompat@20889
OutSrcollView -> null
LinearLayoutCompat@20889 -> null
InnerScrollView -> null

所以,mFirstTouchTarget暂时可以看做是一个依赖于View Hierarchy的链表,终究的item便是当时的工作的接收者,假定现在有布局构成的mFirstTouch链:A->B->C->D->E,此刻的工作就由E消费:

[Android]触摸、滑动与嵌套滑动(二)几种场景下的事件处理分析和调试

假如此刻C要阻拦工作,该链条就变成了A->B->C、D、E,工作由C来消费,DE不再在mTouchTarget构成的一个链之上:

[Android]触摸、滑动与嵌套滑动(二)几种场景下的事件处理分析和调试

3. 子View自动抛弃工作

假定在某个场景之下,View自动抛弃了工作,依据工作传递机制的特性,此刻的工作会开端上浮给上层的视图,例如:A->B->C->D->E,这五个控件构成的视图树,假如E抛弃了ACTION_DOWN工作,那么工作会上浮到D的dispatchTouchEvent中,表现为E的dispatchTouchEvnet办法调用弹出办法栈。

假如此刻D要消费工作,则会在onTouchEvent中回来true,否则回来false,后者则会持续走上述的流程,上浮到C。

那么假如是E承受了ACTION_DOWN,然后自动抛弃了某一个ACTION_MOVE,接下来会产生什么呢?

其实置疑的点,就在于E抛弃了ACTION_MOVE工作之后,E还能不能收到后续工作,假如收不到,后者是D会不会像面对ACTION_DOWN被E抛弃了相同,去从头接收工作。

InnerScrollView(86112637,), scolledY:499
CustomViewGroup(149935399), dispatchTouchEvent:ACTION_MOVE
OutScrollView(70898644,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), onTouchEvent:ACTION_MOVE,consumed:true
InnerScrollView(86112637,), scolledY:504
// 上一次的滑动之后,偏移量达到了504,
// 下一次滑动开端时,ScrollY将超出500,超出500之后,InnerScrollView的onTouchEvent将会回来false,即不再处理;
CustomViewGroup(149935399), dispatchTouchEvent:ACTION_MOVE
OutScrollView(70898644,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), onTouchEvent:ACTION_MOVE,consumed:false
InnerScrollView(86112637,), scolledY:504
// next
CustomViewGroup(149935399), dispatchTouchEvent:ACTION_MOVE
OutScrollView(70898644,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), dispatchTouchEvent:ACTION_MOVE
InnerScrollView(86112637,), onTouchEvent:ACTION_MOVE,consumed:false
InnerScrollView(86112637,), scolledY:504

咱们可以清楚地看到,即便咱们的InnerScrollView在某一次的ACTION_MOVE中,在onTouchEvent回来false之后,尔后的ACTION_MOVE依然会下发到InnerScrollView上, 对应着上面的例子,ABCDE五个控件中,假如E抛弃了ACTION_MOVE之后,依然可以收到ACTION_MOVE工作,也便是说mFirstTouchTarget构成的链表没有断开的状况下,E控件仍是能收到工作的。

mFirstTouchTarget的定义也越来与明朗了,便是当时ViewGroup在一个滑动过程中(按下,滑动,抬起)第一次接触的View控件,每个ViewGroup的mFirstTouchTarget构成的链表的终究一项便是工作的消费者,假如中间的VewGroup自动去阻拦工作,那么就会将余下的链表项目切断,自己成为终究一个ListNode来消费工作

假如控件开端不承受ACTION_DOWN工作,那么就抛弃了自己被挂载在mFirstTouchTarget上的时机,天然也就没有时机再去承受工作了。

只有首个ACITON_MOVE会依据坐标确认被点击的View,其余的都是依据mFirstTouchTarget的链条来下发的,所以,父ViewGroup阻拦工作的时分,很重要的一件工作便是断开ViewGroup自己的mFirstTouchTarget对下层View的连接。

而对于ACTION_DOWN工作来说,会依据手指触控的坐标,比方(500,500)来确认控件在当时ViewGroup中的方位,这个过程需求按次序遍历一切的子View,直到找到某个子View,并且它可以承受处理(在dispatchTouchEvent和onTouchEvent中回来true)停止。

并且记录为mFirstTouchTarget,后续的无数多个ACTION_MOVE就不需求再次去依据坐标定位了,直接依据当时mFirstTouchTarget的child域,就可以找到下一个ViewGroup或许终究找到ACTION_DOWN的接收者,所以,ACTION_DOWN的下发和其它工作的下发是不相同的,前者是在查找终究消费该工作的View,而ACTION_MOVE/UP等等则是只需求沿着mFirstTouchTarget的链条,不断下发即可。

盯梢ACTION_DOWN工作的下发,咱们可以发现:

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

大概意思便是,在收到ACTION_DOWN工作的时分会先清理既有的TouchTarget数据,也便是mFirstTouchTarget的数据。接着,判断一下是否需求阻拦工作,和FLAG_DISALLOW_INTERCEPT这个标记位相关,子View可以经过一个办法来恳求该ViewGroup不要阻拦,假如设置了之后,对应的disallowIntercept的数据为true,就不会阻拦工作了。

// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
    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 {
    // There are no touch targets and this action is not an initial down
    // so this view group continues to intercept touches.
    intercepted = true;
}

然后经过for循环遍历它的children:

for (int i = childrenCount - 1; i >= 0; i--) {
    final int childIndex = getAndVerifyPreorderedIndex(
            childrenCount, i, customOrder);
    final View child = getAndVerifyPreorderedView(
            preorderedList, children, childIndex);
            ……

你会发现,这儿的for的起始下标是childrenCount – 1,也便是说for是倒着遍历的,为什么?

在一个视图中,假如咱们在XML中,按照如下的次序编写视图:

<FrameLayout>
    <A />
    <B />
    <C />
    <D />
    <E />
</FrameLayout>

此刻假如A的尺度最大,B次之,E最小,在烘托出来后应该是这样的视图:

[Android]触摸、滑动与嵌套滑动(二)几种场景下的事件处理分析和调试

由于A是排在最靠前的,所以A会先被烘托出来,B次之,所以B盖在A上方,E在最上方。可是假如咱们点击一个View,比方咱们点击E,假如View的参加次序去遍历,工作会先派发给A。所以这儿View的摆放次序和咱们的点击工作派发次序是反着的,越靠上层的View越晚被参加视图,视觉上也就越靠上,天然而然也应该优先呼应点击工作。

也便是说,View被显现的次序越靠后,在视图层上就越靠上(靠近人眼),承受工作的优先级就越高。可是这个高 = View越晚烘托

回到正题,getAndVerifyPreorderedIndex其实是依据View在children中的下标,取出绘制的次序。可是假如你运用getChildDrawingOrder重写了Draw的次序,getChildDrawingOrder取出来的值就会产生改动, getAndVerifyPreorderedView便是去取View了。

在子View承受工作之后,回来到此处,调用addTouchTarget办法增加FirstTouchTarget。

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

而ACTION_MOVE,便是直接经过mFirstTouchTarget的child域来下发的:

if (dispatchTransformedTouchEvent(ev, cancelChild,
        target.child, target.pointerIdBits)) {
    handled = true;
}

4. 总结

上面咱们剖析了工作传递机制的三种十分常见的状况:

  1. 完整而正常的一次滑动;
  2. 父View自动阻拦的工作派发;
  3. 子View自动抛弃工作的派发(主要是ACTION_MOVE工作)

还有mFirstTouchTarget在工作传递机制中的效果。

进一步验证了在(一)中,咱们总结出来的几个规则:

  1. ACTION_DOWN工作优先派发给叶子节点的View,必须确保叶子节点的View可以有挂在mFirstTouchTarget链上的时机;
  2. 假如一个控件不承受ACTION_DOWN工作,那后续就不会得到ACTION_MOVE和ACTION_UP, 由于后续工作的下发是依据mFirstTouchTarget的child域下发的,假如在ACTION_DOWN中没有把自己挂在链上,后续的工作就没有时机再去消费了。
  3. 即便一个控件消费了ACTION_DOWN工作,也不意味着它就能收到本次接触后续的一切工作。ACTION_MOVE工作在父View滑动和子View点击产生冲突的时分,或许会被父View阻拦,并向子View派发ACTION_CANCEL工作,尔后的ACTION_MOVE工作由父View来消费。
  4. 即便一个控件没有消费ACTION_DOWN工作,也或许会经过阻拦工作的方法消费后续的ACTION_MOVE工作。

~end