Android版本:7.0(API27)
[TOC]
Activity对点击事件的分发
当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发,具体的工作由Activity内部的Window来完成。Window会将事件传递给decor view,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。我们先从Activity的dispatchTouchEvent分析。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回fasle,那么Activity的onTouchEvent就会被调用。
接下来看Window是如何将事件传递给ViewGroup的。通过源码我们知道,Window是个抽象类,而Window的superDispatchTouchEvent方法也是个抽象方法,因此我们必须找到Window的实现类才行。
Window.superDispatchTouchEvent
public abstract boolean superDispatchTouchEvent(MotionEvent event);
那么到底Window的实现类是什么?其实是PhoneWindow,这一点从Window的源码中也可以看出来,在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
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow将事件直接传递给了DecorView,这个DecorView是什么呢?请看下面:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
我们知道,通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式就可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。目前事件传递到了DecorView这里,犹豫DecorView继承自FrameLayout且是父View,所以最终事件传递给View。换句话说,事件肯定会传递到View,不然应用如何响应点击事件?所以,重点就到了ViewGroup中事件分发机制了。
ViewGroup事件分发源码解析
基础知识:
- 由于一个完整的事件序列是以down开始,以up结束;
- 哪一个view消费了down事件,那么后续事件都将交给它处理(如果它的父view不拦截事件);
事件分发会调用ViewGroup.dispatchTouchEvent方法,该方法的核心是按照如下步骤分析:
- down事件的分发;
(1)ViewGroup是否拦截;
(2)ViewGroup中是否有子View消费down事件; - 非down事件分发;
重要的字段
该方法中有两个重要的字段:
intercepted:ViewGroup是否拦截事件,true表示拦截;
mFirstTouchTarget:是否有子View消费了down事件,如果有则mFirstTouchTarget != null;
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();
}
首先这里先判断事件是否为DOWN事件,如果是,则初始化,resetTouchState方法中把mFirstTouchTarget置为null。由于一个完整的事件序列是以DOWN开始,以UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。
这里的mFirstTouchTarget非常重要,后面会说到当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素,这里要留意一下。
检查ViewGroup是否要拦截事件
// 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;
}
通过源码分析,在如下两种情况下会调用ViewGroup的nInterceptTouchEvent检查ViewGroup是否拦截事件:
- 事件序列的开始事件ACTION_DOWN;
- 子View消耗了ACTION_DOWN事件,非DOWN的事件都会经过ViewGroup的nInterceptTouchEvent,看看ViewGroup是否需要拦截;
如果ViewGroup拦截了事件则intercepted = true。
注:此处还有一个FLAG_DISALLOW_INTERCEPT属性,为了流程清晰,我们暂且不分析,后续再说明。
ViewGroup不拦截事件
如果ViewGroup不拦截事件且事件是ACTION_DOWN事件,就会执行这个双if语句中:
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) {
}
}
这个双if语句的核心作用是:将ACTION_DOWN分发到自己的子View中,找到消费DOWN事件的子View,并赋值给mFirstTouchTarget;如果没有子View处理down事件那么mFirstTouchTarget == null。
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 = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) { // ------------------------------------1
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(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)) {// -----------------------2
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)) {// -----------------------3
// 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();
}
这里获取了childrenCount的值,表示该ViewGroup内部有多少个子View,如果有子View就开始遍历所有子View判断是否要把事件分发给子View。
代码也比较长,我们只关注重点部分:
-
代码标记1
是一个for循环,这里表示对所有的子View进行循环遍历,由于以上判断了ViewGroup不对事件进行拦截,那么在这里就要对ViewGroup内部的子View进行遍历,一个个地找到能接受事件的子View,这里注意到它是倒序遍历的,即从最上层的子View开始往内层遍历,这也符合我们平常的习惯,因为一般来说我们对屏幕的触摸,肯定是希望最上层的View来响应的,而不是被覆盖这的底层的View来响应,否则这有悖于生活体验。 -
代码标记2
根据方法名字我们得知这个判断语句是判断触摸点位置是否在子View的范围内或者子View是否在播放动画,如果均不符合则continue,表示这个子View不符合条件,开始遍历下一个子View -
代码标记3
这里调用了dispatchTransformedTouchEvent()方法,这个方法有什么用呢?
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
当child != null时,将down事件传递到子view的dispatchTouchEvent;否则由ViewGroup的父类dispatchTouchEvent处理。 前面的分析条件是有子View的情况,所有child != null,down事件传递到子view的dispatchTouchEvent处理。
如果子View的onTouchEvent()返回true,那么就是消耗了Down事件,接着会调用addTouchTarget方法:
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
该方法中对mFirstTouchTarget = target进行了赋值,这也证实了前面所说的 “如果子View消耗了事件,那么mFirstTouchTarget不为null”。
小结:
整一个if(!canceled && !intercepted){ … }代码块所做的工作就是对ACTION_DOWN事件的特殊处理。因为ACTION_DOWN事件是一个事件序列的开始,所以我们要先找到能够处理这个事件序列的一个子View,如果一个子View能够消耗事件,那么mFirstTouchTarget会指向子View,如果所有的子View都不能消耗事件,那么mFirstTouchTarget将为null。
分发非ACTION_DOWN之外的其它事件
上面代码的整个过程目的只有一个,找到消费down事件的子View,如果找到mFirstTouchTarget != null。下面就是根据mFirstTouchTarget对move、up事件进行分发。
// 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;
}
}
(1)mFirstTouchTarget == null
说明没有子View消耗down事件,那么后续所有事件都交给ViewGroup的super.dispatchTouchEvent去处理;
(2)mFirstTouchTarget != null
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)
这里的判断就是区分了ACTION_DOWN事件和别的事件,因为在上面的分析我们知道,如果子View消耗了ACTION_DOWN事件,那么alreadyDispatchedToNewTouchTarget和newTouchTarget已经有值了,所以就直接置handled为true并返回;那么如果alreadyDispatchedToNewTouchTarget和newTouchTarget值为null,那么就不是ACTION_DOWN事件,即是ACTION_MOVE、ACTION_UP等别的事件,这些事件都会传递给消耗down事件的子View即mFirstTouchTarget。
FLAG_DISALLOW_INTERCEPT设置
FLAG_DISALLOW_INTERCEPT标志位是通过requestDisallowInterceptTouchEvent方法来设置
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
一旦设置了FLAG_DISALLOW_INTERCEPT为true,那么ViewGroup就不能通过onInterceptTouchEvent返回true来拦截任何事件了。这一点在分析"检查ViewGroup是否要拦截事件"中可以看到。