参考文献:
https://www.jianshu.com/p/e6ceb7f767d8
https://www.jianshu.com/p/d3758eef1f72
https://www.cnblogs.com/linjzong/p/4191891.html
https://www.jianshu.com/p/8236278676fe
https://www.jianshu.com/p/1378b334ee85
自篇仅作为学习笔记的总结,
1、Android的事件分发是指用户在手机屏幕上产生的交互事件的传递,比如点击(包含按下down和抬起up)、移动(move)等。而在Android系统中使用类MotionEvent表示这些产生的交互事件。
2、需要分发的事件(对象)有了,接下来就是做事件分发的主体了,即谁来进行MotionEvent的事件分发,并分发(传递)给谁?
要回答这个问题,就涉及了三个主要的类及其各自相应的方法:
三个类:Activity ViewGroup View
方法:public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent event);
ViewGroup包含了上述三个函数,Activity和View不包含onInterceptTouchEvent方法。单从函数名称可以推断各自所执行的功能:分发事件、拦截事件、处理事件。
现在需要关注两个问题:一、三个类的七个相关函数执行的先后顺序;二、其返回值所代表的意思。
对于问题一,三个类的七个相关函数不一定会在一次交互事件中全部执行到,是否会执行,这取决于上一个执行函数的返回值。所以,对于问题二,可以提前给出结论:返回Ture表示MotionEvent事件被当前类(准确来说是Activity或视图)消费掉,不再向下传递分发;返回False表示MotionEvent事件目前还没有被消费(传递过来的路上没有被消费,而且当前的Activity或视图也不消费它),那么MotionEvent事件将继续传递分发,按照问题一的结果(执行先后顺序)继续。
问题再次回归:七个相关函数的执行顺序!
下面引用参数文献中的示例图进行说明:
根据这个图再来谈顺序就显得简单了许多:ViewGroup对事件进行分发,也进行拦截(当然可选事件类型,如MotionEvent.ACTION_DOWN或者MotionEvent.ACTION_UP等),子View对事件进行分发(其实就是给它自己),多然后执行事件处理;如果不能处理(及不能消费掉该事件,准确来说,该事件不应该由该子View处理),其返回值必为False。此时由ViewGroup接管,执行ViewGroup的事件处理函数(onTouchEvent函数)。
现在执行顺序就很清晰了。最先执行的是Activity的dispatchTouchEvent方法,最后执行的是Activity的onTouchEvent方法,中间穿插着ViewGroup和View的相关方法。而这中间的顺序,正如上段文字描述的那样,由ViewGroup分发,并做拦截,如果ViewGroup进行了拦截,则没有子View什么事,直接执行ViewGroup的onTouchEvent方法;如果ViewGroup没有拦截,那么事件继续向下传递。然后由其子View分发并做处理,如果子View不消费,就要由ViewGroup来处理,执行onTouchEvent方法(儿子收拾不了的烂摊子,当然要由父母来处理)。【这里引用父子比喻是指容器的关系,而非继承关系(ViewGroup继承View)】
3、再谈下MotionEvent的事件。触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。这里补充下其实UP事件是可能为0个的。
基本上文字的结论性描述可以告一段落了。作为文字结论的结尾,再引用一下参考文献里给出的图,然后再谈下源码。
上图可以作为文字结论的图形说明。
得出这些结论,是基于对源码的分析,再展开源码分析之前,我在这里先记录下查看源码的小插曲——Android Studio没法查看源码的解释方案。
当我想看源码,发现跟进去显示的是.class文件。百度了方案,可以概括为以下几点:
首先,检查android sdk目录下的sources文件夹是否包含相应API的文件。其次,核对C:\Users\用户名\.AndroidStudio3.0\config\options目录下jdk.table.xml文件下对应的内容,是否指向了对应的<root><sources></sources></root>。然后打开Android Studio的SDK设置,进行安装关联,如图:
基本上就可以解决了。最后开启源码之旅。
既然提到了Activity,那么就先从Activity的dispatchTouchEvent方法谈起:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
当MotionEvent对象的值是ACTION_DOWN时,会执行onUserInteraction();方法,该方法是一个空的方法,没有任何实现。参考文献中指出:该函数的作用是屏保,当此 Activity 在栈顶的时候,触屏点击 Home、Back、Recent 键等都会触发这个方法。那么接下来的重点就是两个不同的Return返回值。这两个返回值正常反映了上面的文字结论描述:最先执行Activity的分发函数,最后执行Activity的onTouchEvent函数。
当我们跟着if语句追下去时,就到了ViewGroup的dispatchTouchEvent()方法。而这个过程有必要提及一下(我只看了第一篇参考文献时,并不清楚为什么会就到了ViewGroup的dispatchTouchEvent()方法)。
getWindow获得的是Window类(android.view.Window)对象,该类的定义是一个虚拟类,superDispatchTouchEvent方法也是一个虚拟方法,我们现在找到它的实现。看下Window类的源码:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
...
}
上段截的代码,只有一句有价值(对于找到方法的实现):存在唯一一个实现类PhoneWindow。自然,我们可以找到这个类,也就找到了该类对superDispatchTouchEvent方法的实现。
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
显然,我们又需要找DecorView的相应方法了。DecorView是当前界面的底层容器,它的方法是调用了super.dispatchTouchEvent方法。这里使用了super,自然要去它的父类去找,最终指向了ViewGroup。
下面就是两根难啃的骨头:ViewGroup的三个函数,以及View的两个函数。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 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();
}
// 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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
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;
}
}
}
// 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);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
长长的代码,可以进行拆分:最前面的二句if语句可以忽略掉(我所阅读的参考文献都没有提到这些语句的作用,我记下自己的理解,可以存在偏差)。mInputEventConsistencyVerifier变量是ViewGroup从View继承过来的,而在View中定义上面有一行注释:意思是用于调试一致性的。代码如下:
//View.java
/**
* Consistency verifier for debugging purposes.
* @hide
*/
protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
InputEventConsistencyVerifier.isInstrumentationEnabled() ?
new InputEventConsistencyVerifier(this, 0) : null;
紧接着第二个if语句其实也是不影响结果的,根据函数返回值为handled变量来看,这两句if判断语句执行时,handled变量还没定义呢。所以,许多文章对于进行了忽略。
既然,函数返回值是handled变量,那么,我们研读的方向自然是跟着handled变量的定义、使用、赋值等过程来追踪。
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
}
这个大的IF语句通常一定会执行,因为内部才是其核心的处理流程,不执行这个IF里面的内容,那么,函数直接return刚定义了handled变量了。这个判断是一个安全过滤。当视图没有被覆盖时才会执行。(比如,正在的切换屏幕,有一部分存在遮挡之类的情况,是不执行的。)
内部的核心代码之一:
// 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;
}
影响intercepted变量值的除了手动赋值外,就是我们提到的三个核心函数之一:onInterceptTouchEvent(ev);显然它并不是每次都会执行,简言之,它的执行是需要If条件的。由于onInterceptTouchEvent(ev)拦截函数代码比较简单,先提下这个函数的源码,然后再来理清它执行时所需要的条件。
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
长长的注释,而代码就两行(注释不翻译了)。这也是许多文章中提到,如果不重写,它的返回值在绝大多数都为false,即ViewGroup不进行拦截,后续执行子View的分发、处理事件的两个函数。(得出这一结论,当然也是基于源码,后面会分析intercepted变量的使用)
现在来清理执行它所需要的条件。一个条件判断if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)和一个disallowIntercept变量。
条件判断:
MotionEvent.ACTION_DOWN事件已经说明了,是Event的状态之一(还有move/up之类的);mFirstTouchTarget变量不为空,那么它代表着什么呢?分支代码(及不是事件分发主流程的代码)表示,当ViewGroup的点击事件被子View消费,mFirstTouchTarget变量就会指向该子View,此时值不为空。为什么要做这个判断?感觉还是有必要再提一下,核心代码上面的几行代码,如下:
// 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();
}
其做的处理是,当为Down时,清空mFirstTouchTarget变量。因为Down是一个触摸事件的开始,所以对于新的MotionEvent事件要初始化一下它的状态。由这两行代码,我们就可以推测出IF判断的作用了,即回答为什么要做这个判断。
如果是Down事件,那ViewGroup可能需要判断是否要拦截;如果不是Down事件,但是又想拦截它(比如是Move事件),那么if分支依然可能走到(如果消费了Down事件的子View存在,则此时mFirstTouchTarget变量在非空)。而如果子View没有消费Down事件,那么这一次的事件序列(如Move事件),子View也肯定不能消费,所以没必要向子View传递,直接返回True。
disallowIntercept变量:
需要关注一个标志位:FLAG_DISALLOW_INTERCEPT,禁止ViewGroup拦截除了DOWN之外的事件。一般通过子View的requestDisallowInterceptTouchEvent来设置。
如果没有被禁止拦截,当然是要做拦截处理的(即执行onInterceptTouchEvent函数)。
至此,内部核心代码之一分析完成。引出内部核心代码之二的就是前面提到的intercepted变量。
if (!canceled && !intercepted) {
...
}
没有被取消且没有被拦截,ViewGroup的dispatchTouchEvent函数执行的是子View的遍历。
想了下,子View遍历展开的话,文章有点长了,正好结尾,另起开头。
ViewGroup和View的源码见另一篇。