本文用于记录各种场景下的工作承受、工作突然被父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 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是什么?
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消费:
假如此刻C要阻拦工作,该链条就变成了A->B->C、D、E,工作由C来消费,DE不再在mTouchTarget构成的一个链之上:
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最小,在烘托出来后应该是这样的视图:
由于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. 总结
上面咱们剖析了工作传递机制的三种十分常见的状况:
- 完整而正常的一次滑动;
- 父View自动阻拦的工作派发;
- 子View自动抛弃工作的派发(主要是ACTION_MOVE工作)
还有mFirstTouchTarget
在工作传递机制中的效果。
进一步验证了在(一)中,咱们总结出来的几个规则:
- ACTION_DOWN工作优先派发给叶子节点的View,必须确保叶子节点的View可以有挂在mFirstTouchTarget链上的时机;
- 假如一个控件不承受ACTION_DOWN工作,那后续就不会得到ACTION_MOVE和ACTION_UP, 由于后续工作的下发是依据mFirstTouchTarget的child域下发的,假如在ACTION_DOWN中没有把自己挂在链上,后续的工作就没有时机再去消费了。
- 即便一个控件消费了ACTION_DOWN工作,也不意味着它就能收到本次接触后续的一切工作。ACTION_MOVE工作在父View滑动和子View点击产生冲突的时分,或许会被父View阻拦,并向子View派发ACTION_CANCEL工作,尔后的ACTION_MOVE工作由父View来消费。
- 即便一个控件没有消费ACTION_DOWN工作,也或许会经过阻拦工作的方法消费后续的ACTION_MOVE工作。
~end