事件分发和NestedScrolling(一)
来源:互联网 发布:qq输入法 linux 编辑:程序博客网 时间:2024/06/06 01:41
前言
关于事件分发好像都已经说的很多了,网上也有很多资料,本来这篇文章主要是讲NestedScrolling
(嵌套滚动),但是因为它和事件分发的相关性较大,所以还是讲一下。
事件分发的三个核心方法如下:
1、dispatchTouchEvent():分发事件2、onInterceptTouchEvent():决定父View是否拦截该事件不交由子View处理3、onTouchEvent():消费事件
这三个方法的关系可以用下面这段伪代码来表示:
//ViewGrouppublic boolean dispatchTouchEvent(MoveEvention event){ boolean result; if(!disallowIntercept && onInterceptTouchEvent(event)){ result = onTouchEvent(event); }else{ result = child.dispatchTouchEvent(event); } return result;}//子Viewpublic boolean dispatchTouchEvent(MoveEvention event){ boolean result = onTouchEvent(event); return result;}
看得多了,每次说起来也倒背如流,但是除了这些,关于事件分发的每个细节大家是否都足够了解呢?
比如,子View接受触摸事件之后,父View真的不能再干涉了吗?父View拦截子View的事件之后,子View真的收不到任何事件了吗?事件冲突要怎么解决?最后的最后,知道普通的事件冲突有什么不完美的地方吗?
如果都不了解,或者有些不了解,那么恭喜你,这篇文章正好是写给你看的。
事件处理机制
说之前先和大家达成几个共识:
事件分发时由外向里,抛出时由里向外。 即分发时,事件先经过父View,然后到达子View;抛出时,先从子View然后到父View。
如果一个事件能到达该View,则一定会先走该View的dispatchTouchEvent()方法
父View一旦拦截事件,则不会再次调用onInterceptTouchEvent(),直接处理后续到来的事件。
子View一旦处理了ACTION_DOWN事件,则后续的所有事件都会交由它处理,否则后面的事件都不会交由它处理。 所以如果一个触摸事件父View想让子View处理,就一定不能拦截子View的
ACTION_DOWN
事件。
一时消化不了没关系,让我们一条条的来看对照着看源码:
1、父View一旦拦截事件,则不会再次调用onInterceptTouchEvent(),直接处理后续到来的事件
ViewGroup
中dispatchTouchEvent()
方法的源码如下:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ... // 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; } ... // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } 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) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } ... }
代码第6~7行,如果当前是ACTION_DOWN
事件,或者不是ACTION_DOWN
但是已经有子View在处理事件,则判断是否需要拦截事件。这个很好理解,原因如下:
如果当前是
ACTION_DOWN
事件,则说明开始了一个新的触摸事件需要开始新的分发流程,所以需要重新判断是否要拦截如果当前正在一个分发流程当中,且
mFirstTouchTarget!=null
(mFirstTouchTarget
是单链表,指针指向的是当前触摸事件的触摸链表中的第一个触摸目标,它不为null
说明当前可以找到能够消费事件的子View),则需要判断是否要拦截这个事件
其他不需要拦截的情况是:如果当前没有子View处理,当然是不需要拦截,直接走正常的分发流程,自己处理消费。
代码8~14行,上面判断了是否需要拦截,这里则判断是否能够拦截,因为子View可以禁止父View拦截触摸事件,如果有子View禁止了,这里则不能拦截了。
其他情况则默认拦截。
决定完是否需要拦截后,接下来对当前是否有子View正在处理事件分别进行处理。
代码第24~28行,如果当前没有子View处理事件,则直接走自己的事件分发流程。
第31~57行,则考虑有子View处理事件的情况。用while
循环遍历mFirstTouchTarget
单链表,依次调用dispatchTransformedTouchEvent
方法对单链表中所有可能消费该事件的子View发送取消消费的事件,当父View确定要拦截事件的话,这里cancelChild
的值是true
,所以下面方法的形参cancel
也是true。
第61~65行,取消完子View之后,调用resetTouchState()
方法:
/** * Resets all touch state in preparation for a new cycle. */ private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }
方法里面又调用resetTouchState()
方法:
/** * Clears all touch targets. */ private void clearTouchTargets() { TouchTarget target = mFirstTouchTarget; if (target != null) { do { TouchTarget next = target.next; target.recycle(); target = next; } while (target != null); mFirstTouchTarget = null; } }
最终将mFirstTouchTarget
置为null
,所以下次当手指没有抬起继续在屏幕上滑动时,走进dispatchTouchEvent()
方法判断是否需要拦截时,由于事件既不是ACTION_DOWN
,mFirstTouchTarget!=null
也不成立(resetTouchState
方法中已经置为null
),if
的两个条件都不满足,所以intercepted
的值很直接的就是true
了。
所以,父View一旦拦截事件,则不会再次调用onInterceptTouchEvent(),这个共识我们已经达成了,因为一旦决定拦截,resetTouchState
方法中就会将mFirstTouchTarget
置为null
,导致父View认为当前事件没有子View需要处理,当然不需要拦截所以也无需进入拦截的流程,默认自己消费。
那子View一旦处理了ACTION_DOWN事件,则后续的所有事件都会交由它处理,否则后面的事件都不会交由它处理。 这个怎么说呢?
2、子View一旦处理了ACTION_DOWN事件,则后续的所有事件都会交由它处理,否则后面的事件都不会交由它处理
在dispatchTouchEvent()
方法中,有如下代码:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ... if (!canceled && !intercepted) { ... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } ... }
第5~11行,如果事件没被取消且父View不拦截,则开始寻找可以消费该事件的子View。
第15~25行,循环遍历所有子View,依次寻找。
第35~41行,如果子View不能被focus,则跳过该子View,继续寻找。
第43~47行,如果事件发生的坐标不在该子View显示的区域内,则跳过该子View,继续寻找。
第58~77行,经过上面两步,在dispatchTransformedTouchEvent
方法中尝试将该事件分发给该子View,如果分发成功,则认为该子View可以消费当前事件。
代码74行,将该子View加入可消费该事件的链表内。
若找到,则停止for
循环,否则继续寻找。
也很清楚,在父View不拦截的情况下,mFirstTouchTarget
指向的单链表中存储了可以消费当前事件的所有子View,如果有触摸事件且父View不拦截的情况下,父View分发时会循环遍历mFirstTouchTarget
指向的链表中所有的子View,直到找到能够消费该事件的子View为止。详见上面代码31~57行。
所以,一旦mFirstTouchTarget
不为null
,则事件分发时就会在mFirstTouchTarget
指向的链表中寻找可以消费事件的子View,换句话说,父View
分发事件时,要么在mFirstTouchTarget
指向的链表中寻找子View
来消费,要么自己消费。
当ACTION_DOWN
事件到来时,如果子View
消费了,就会存储在mFirstTouchTarget
指向的单链表中,后面的事件到来时就会被父View
找到并且分发;如果不消费,就不会在链表中,后面的事件就不会被父View
分发。
因此,子View一旦处理了ACTION_DOWN事件,则后续的所有事件都会交由它处理,否则后面的事件都不会交由它处理
3、子View接受触摸事件之后,父View真的不能再干涉了吗?
答案:不是。
子View接受触摸事件之后,该View被存储在mFirstTouchTarget
指向的单链表中,当事件到来时,dispatchTouchEvent()
方法的流程是:先检查父View要不要拦截,然后再循环遍历mFirstTouchTarget
单链表。
因此,只要子View没有禁止父View拦截事件,父View在任何时机都可以拦截掉事件,让子View不再消费。
所以,子View接受触摸事件之后,父View真的不能再干涉了吗?这是不对的。
4、父View拦截子View的事件之后,子View真的收不到任何事件了吗?
答案:不是。
回到代码:
public boolean dispatchTouchEvent(MotionEvent ev) { ... // 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; } ... // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } 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) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } ... }
代码5~19行,父View决定拦截事件时,得到的intercepted
值为true
。
24~28行,如果子View没有消费事件,则直接分发给自己。当然这里只考虑有子view消费事件的情况,所以不是走这里。
31~57行,这里考虑有子Viwe消费事件时,父View拦截事件时的情况。循环遍历所有的子View,并对其分发该事件。
38~43行,考虑是否要取消子View对该事件的消费,由于父View拦截事件时intercepted
的值是true
,所以这里cancelChild
的值也是true
,然后调用dispatchTransformedTouchEvent()
方法
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ 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; } ... }
代码13~22行,由于这里cancel
的值是true
,则把当前事件的action改成MotionEvent.ACTION_CANCEL
,然后分发给子View。
所以就很明朗了,当父View决定拦截事件后,子View会收到ACTION_CANCEL
的事件,然后父View会将可消费当前事件序列的子View信息(即mFirstTouchTarget
指向的单链表)清空,所以下次触摸事件再次到来的时候,父View会直接消耗该事件。
因此,父View拦截子View的事件之后,子View真的收不到任何事件了吗?这是不对的,起码子View还会在被拦截的那一刻收到ACTION_CANCEL
的事件。
子View不可能一直停留在ACTION_MOVE
的状态,不管有没有被拦截,事情总归有头有尾对吧。虽然有点标题党了,但是这样一看是不是理解的更加深刻了呢?
处理事件冲突
上面说了事件分发的很多条准则,也是看源码总结出来的规律,接下来看看事件冲突的解决方案。
首先,事件冲突发生的场景主要有下面三种:
相同方向冲突:
ViewPager
+SwipeBackLayout
不同方向冲突:
ViewPager
+RecyclerView
上面两种同时出现
很多人碰到事件冲突可能觉得一脸茫然,无从下手。其实解决事件冲突有两种固定的方法,掌握了这两种方法,以后碰到事件冲突的问题基本上可以迎刃而解了。
这两种解决方案都有一个主动方来决定是否拦截事件,根据决定拦截的主动方可以分为外部拦截
和内部拦截
,即:
- 父View决定是否拦截时,称为外部拦截
- 子View决定是否拦截时,称为内部拦截
外部拦截
父View决定是否拦截事件,这个很简单,因为事件分发的机制本来就是分发时由外到里,抛出时由里到外,事件本是先经过父View,然后到达子View,所以如果父View想要拦截事件,直接在onInterceptTouchEvent()
中返回true
就可以了。
//父Viewpublic boolean onInterceptTouchEvent(MotionEvent ev){ if(ev.getAction() == MotionEvent.ACTION_DOWN){ //如果拦截了ACTION_DOWN,那就拦截了所有的事件,子View没法接受事件 //也永远无法滚动了 return false; } if(父View需要处理事件){ return true; } return super.onInterceptTouchEvent(ev);}
内部拦截
子View决定是否拦截事件,大致方案是:父View始终拦截除了ACTION_DOWN
以外的事件,子View在dispatchTouchEvent()
事件中控制是否禁止父View拦截事件。
//父Viewpublic boolean onInterceptTouchEvent(MotionEvent ev){ if(ev.getAction() == MotionEvent.ACTION_DOWN){ //如果拦截了ACTION_DOWN,那就拦截了所有的事件,子View没法接受事件 //也永远无法滚动了 return false; } return true;}//子Viewpublic boolean dispatchTouchEvent(MotionEvent ev){ int action = ev.getAction(); switch(action){ case MotionEvent.ACTION_DOWN: //禁止父View拦截ACTION_DOWN事件(拦截了子View就废了) getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if(子View不需要处理事件了){ //打开父View可以拦截的开关,从此事件交给父View处理 getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; } //要返回true,否则收不到后面的事件了 return true;}
子View决定是否拦截事件,说的更准确一点,其实是子View控制父View是否可以拦截子View的事件。
相对于外部拦截
,这种方式稍难理解一些,因为和普通的分发流程是背道而驰的,但是理解之后会对事件分发机制有更加全面和深入的理解。
事件分发不完美之处
这里主要说的是解决事件拦截的部分,前面说了:
- 对于外部拦截,父View一旦拦截事件,则不会调用
onInterceptTouchEvent
方法,会直接消费后面的事件 - 对于内部拦截,子View一旦打开允许父View拦截事件的开关,父View也会直接消耗完后续的所有事件,子View无法重新夺回掌控权
也就是说,父View一旦拦截了事件,子View无法重新再消费事件了。(出发手指重新抬起再按下。)
那有什么完美的解决方案呢?这就需要引出来我们这篇文章的主角了—-NestedScrolling(嵌套滚动)。
小结
说了这么多,把事件分发的整个流程总结一下:
事件分发时由外向里,抛出时由里向外。
如果一个View可以收到触摸事件,则一定会走到它的
dispatchTouchEvent()
方法。如果一个View想要收到完整的触摸事件,则它或者它的子View在
ACTION_DOWN
到来的时候要返回true
,否则不会收到后续的事件了,因为不处理ACTION_DOWN
的时候该View不会被存储在mFirstTouchTarget
链表中,下次分发事件的时候就不会被考虑到,如果mFirstTouchTarget
中一个子View都没有,父View则会直接拦截事件进行消耗。你可能会问,子View消费事件的时候是子View对
ACTION_DOWN
返回true
啊,父View没有返回,为什么父View还会收到后面的事件,然后分发给子View呢?上面说了分发时由外向里,抛出时由里向外,你可能只懂了前半句,后半句说的是,子View抛出的结果是先经过父View,然后父View的父View,然后是父View的父View的父View一层层抛出去的,一旦子View返回了true
,那它的父View们返回的都是true
,代表他们可以处理这些事件,所以下次当事件再次到达时,会通过这些父View以及mFirstTouchTarget
链表信息对应的找到真正消费的子View,所以并不是子View消费时返回了true
,父View没有返回true
,父View其实也是返回true
的。一旦父View决定拦截事件,
mFirstTouchTarget
指向的链表信息会被重置,子View同时会收到ACTION_CANCEL
的事件,以保住自己不会一直停在ACTION_MOVE
的状态。一旦父View决定拦截事件,则事件不会走到子View,父Viw也不会再调用自己的
onInterceptTouchEvent()
方法,因为mFirstTouchTarget
指向的链表信息已经被重置了。父View是否拦截事件取决于两个条件:1、子View是否禁止了父View拦截事件;2、父View在
onInterceptTouchEvent()
中决定自己是否需要拦截事件。一般情况下,事件处理的接力棒只可能被交换一次:事件先给子View消费,然后父View拦截进行消费。这也是事件拦截的核心思想,无法让父View和子View多次交换拦截。(所以实际体验是在嵌套滑动的View上滑动时,子View滑了一部分交给父View滑,当反方向滑动父View滑了想让子View继续滑动时,会导致先卡顿一下,除非手指抬起再按下时继续滑动,子View才可以继续滑动。)
最后
关于解决事件冲突的两种方案,可以参考这个demo:TouchEvent
关于比事件分发更完美的解决事件冲突的方案—-NestedScrolling
,请看《事件分发和NestedScrolling(二)》
- 事件分发和NestedScrolling(一)
- 事件分发和NestedScrolling(二)
- Android事件分发机制(一) Touch 事件的分发和消费机制
- android事件分发(一)
- Android事件分发 (一)
- 事件分发机制(一)
- 笔记:事件分发机制(一):View的事件分发
- android之View和ViewGroup事件分发机制分析(一)(View的事件分发机制)
- Android事件分发机制(三)事件分发和消费
- 学习View事件分发笔记(一)
- 学习ViewGroup事件分发笔记(一)
- View 事件的分发机制 (一)
- Activity dispatchTouchEvent事件分发--测试(一)
- Activity dispatchTouchEvent事件分发--总结(一)
- android事件分发教程(一):View
- 【UI】【View】View事件分发(一)
- 系列(一) Android 事件分发机制
- View 的事件分发机制(一)
- OJ题目--数字整除
- Day22 --序列流 内存输出流 随机访问流 对象操作流 数据输入输出流 打印流 标准输入输出流 Properties
- posix线程的误区: 线程是否启动
- Day23 --递归
- 第三章 ALDS1_1_A:Insertion Sort 插入排序法
- 事件分发和NestedScrolling(一)
- Day24 --多线程(上)
- 基本最小生成树—Kruskal
- Day25 --多线程(下) 设计模式 GUI
- 一篇文章彻底搞懂java动态代理的实现
- 数字与模拟通信系统第四章——带通信号
- JSON 数据交换
- TeraTerm Language 帮助文档2-[数据类型]
- Day26 --网络编程