一、概述
1.1 事件分发概述
当用户触摸屏幕(View
或 ViewGroup
派生的控件),将产生点击事件(Touch
事件)。Touch
事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent
对象,系统需把这个事件传递给一个具体的 View
去处理(消费)以及处理(消费)的整个过程。即事件传递的过程及处理的整个过程,其可能经过的对象有最上层Activity
,中间层ViewGroup
,最下层View
。
1.2 事件的类型
事件类型 | 具体动作 |
---|---|
MotionEvent.ACTION_DOWN | 按下View(所有事件的开始) |
MotionEvent.ACTION_UP | 抬起View(与DOWN对应) |
MotionEvent.ACTION_MOVE | 滑动View |
MotionEvent.ACTION_CANCEL | 结束事件(非人为原因) |
1.3 什么是事件序列?
从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件是同一个事件序列。
一般情况下,事件序列以DOWN事件开始、UP事件结束,中间有无数的MOVE事件.
1.4 事件分发过程中共同协作的方法。
方法 | 作用 | 调用时刻 | 返回值 |
---|---|---|---|
dispatchTouchEvent() | 分发(传递)点击事件 | 点击事件(Touch)传递给当前View时,dispatchTouchEvent()就会被调用。 | 表示是否消耗当前事件 |
onInterceptTouchEvent() | 拦截点击事件 | 在dispatchTouchEvent()内部调用 | 表示是否拦截当前事件 |
onTouchEvent() | 处理点击事件 | 在dispatchTouchEvent()内部调用 | 表示是否消耗当前事件 |
onInterceptTouchEvent()
方法:如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会再被调用。
onTouchEvent()
方法:如果当前View不消耗此事件,则在同一个事件序列中,当前View无法再次接收到此事件。
上述三个方法的关系可以用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过上述伪代码,我们可以大致了解点击事件的传递规则:对于一个根ViewGroup
来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent
就会被调用,如果这个ViewGroup
的onInterceptTouchEvent
方法返回true
就表示它要拦截当前事件,接着事件会传递给当前ViewGroup
的onTouchEvent
,即交由它处理;如果这个ViewGroup
的onInterceptTouchEvent
方法返回false
就表示不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent
方法就会被调用,如此反复直到事件被最终消耗。
二、事件分发源码分析
首先我们知道,一个点击事件产生后,它的事件传递顺序遵循:
Activity->Window ->View
2.1 Activity 对点击事件的分发过程
点击事件用MotionEvent
表示,当一个点击操作发生时,事件最先传递给当前Activity
,有Activity
的dispatchTouchEvent
来进行事件派发,具体的工作是由Activity
内部的Window
来完成的。Window
会将事件传递给decorView
,decorView
一般就是当前界面的底层容器(即setContentView
所设置的View
的父容器),通过Activity.getWindow.getDecorView()
可以获得。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
/*首先事件开始交给 Activity 附属的 Window 进行分发
如果 getWindow().superDispatchTouchEvent(ev) 为 true 代表整个事件循环结束了
Activity的dispatchTouchEvent就返回ture;
如果 getWindow().superDispatchTouchEvent(ev) 为 false 意味着没人处理,所有 View 的
onTouchEvent() 都返回了 false,那么 Activity 的onTouchEvent 就会被调用。
*/
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
写下来看 Window
是如何将事件传递给ViewGroup
的。
Window
是一个抽象类,其dispatchTouchEvent
()也是抽象方法,该类可以控制顶级View
的外观
和行为策略
。该类唯一实现类是PhoneWindow
,其中有一个内部类DecorView
是一个ViewGroup
。PhoneWindow
中处理点击事件:
public boolean superDispatchTouchEvent(MotionEvent event) {
// mDecor 是 顶级View(DecorView)的实例对象
return mDecor.superDispatchTouchEvent(event);
}
从代码中看出,PhoneWindow
将事件直接传递给了DecorView
。事件此时已经传递到顶层View
了,即在Activity
中通过setContentView
()设置的View
,也叫根View
,此处调用的是ViewGroup
的dispatchTouchEvent
()方法,从而实现了事件由Activity→ViewGroup
的dispatchTouchEvent()
的过程。
ViewGroup
的dispatchTouchEvent
()什么时候返回true
/ false
?接下来ViewGroup
的事件分发流程会进行说明。
2.2 顶级 View 对点击事件的分发过程
首先看ViewGroup
对点击事件的分发过程,其主要实现在ViewGroup
的dispatchTouchEvent
方法中:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 此段代码描述的是当前 View 是否拦截点击事件这个逻辑
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {// ViewGroup 决定拦截执行当前 View 的 onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {// 不拦截,直接false
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 不拦截事件,事件回向下分发交由它的子View进行处理。
if (!canceled && !intercepted) {// 不取消也不拦截
// If the event is targeting accessibility 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 =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : 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();
// 遍历 ViewGroup 的所有子元素
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;
}
// 然后判断子元素是否能够接收到点击事件,能否接收点击事件主要由两点衡量,
// 子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内(均在
// isTransformedTouchPointInView方法中,源码往下翻)
if (!child.canReceivePointerEvents()
|| !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;
}
// 如果哪个子元素能够接收,事件就会传递给它来处理,
// 在 dispatchTransformedTouchEvent 中,由于我们传递的child!=null,
// 所以实际上他会调用子元素的 dispatchTouchEvent,这样事件就交由
// 子元素处理了,从而完成一轮事件分发。(源码往下翻)
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();
// 如果 子元素的 dispatchTouchEvent 返回 true,那么在
// addTouchTarget 中,会完成 mFirstTouchTarget 的赋值并终止子元素的遍历;(源码往下翻)
// 如果子元素的 dispatchTouchEvent 返回 false,ViewGroup就会把事件
// 分发给下一个子元素(如果还有下一个子元素的话)
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);
}// 遍历子元素的 for 循环到这里结束
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.
// 如果遍历了所有的子元素后事件都没有被合适的处理,这包含两种情况:
// 1.ViewGroup 没有子元素
// 2.子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素的 onTouchEvent 中返回了 false。
// 在这两种情况下,ViewGroup会自己处理点击事件,注意看下面的
// dispatchTransformedTouchEvent 方法,第三个参数 child 为 null,从前面的分析知,他会调用
// super.dispatchTouchEvent(event),这里就转到了 View 的 dispatchTouchEvent 方法,即点击
// 事件交由 View 处理。
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;
}
}
...
}
...
return handled;
}
ViewGroup
的isTransformedTouchPointInView
方法源码:
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
// 点击事件的坐标是否落在子元素的区域内
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
// 子元素是否在播放动画
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
ViewGroup
的dispatchTransformedTouchEvent
方法源码:
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;
}
...
}
ViewGroup
的addTouchTarget
方法的源码:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
遍历ViewGroup
的子View
或子ViewGroup
,判断此次点击事件触摸区域是否属于它的子View
或子ViewGroup
的区域,如果属于子View区域
就调用子View
的dispatchTouchEvent()
,即传递给子View
处理,如果属于子ViewGroup
,继续遍历子ViewGroup子视图
,直到找到处理事件的View
。如果遍历了全部子View
事件都没有被处理,则转到 View
的 dispatchTouchEvent
方法,即点击事件交给 View
来处理。
2.3 View 对点击事件的处理过程
首先查看View
的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 先会判断有没有设置 OnTouchListener,
// 如果 OnTouchListener 中 onTouch() 返回true,onTouchEvent() 就不会被调用,
// 可见 OnTouchListener 的优先级高于 onTouchEvent。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// result==true,不执行 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
接着分析onTouchEvent
方法的实现:
public boolean onTouchEvent(MotionEvent event) {
...
// 先看当View处于不可用状态下点击事件的处理过程,很显然,不可用状态下的View照样会消耗点击事件。
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
// 接着,如果 View 设置有代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法,这个
// 方法的工作机制看起来和 OnTouchListener 类似。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
对点击事件
的具体处理:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 当 ACTION_UP 事件发生时,会触发performClick 方法(在
// performClickInternal内部执行,源码如下),如果 View 设置了
// OnClickListener,那么 performClick 方法内部会调用它的onClick 方法
performClickInternal();
}
}
}
...
}
break;
}
return true;
}
return false;
}
View
的perfromClick
方法源码:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
// 如果 View 设置了OnClickListener,那么 performClick 方法内部会调用它的onClick 方法
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}