Android事件分发机制,也被称为“事件处理机制”,“Touch处理机制”等等,指的是对用户触摸屏幕后引发的一系列Touch事件的处理机制,例如用户点击一次屏幕,该事件到底是页面上的哪个View来处理,为什么?
MotionEvent事件主要有:down,move,up
主要分析的类:ViewGroup,View和特殊的View(ListView,ScrollView等)
主要分析的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent
知识背景
Activity管理着一个Window,具体的实现类PhoneWindow,PhoneWindow里面有个DecorView的对象,DecorView分为两部分,title和content。我们在Activity里面调用setContentView,就是给content部分加载布局
事件传递流程源码分析(API 27)
- 先看Activity.java,在dispatchTouchEvent中,会执行getWindow().superDispatchTouchEvent(ev),调用mWindow的superDispatchTouchEvent(ev),即将事件传递给PhoneWindow
// Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// getWindow()放回mWindow对象,调用mWindow的superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public Window getWindow() {
return mWindow;
}
private Window mWindow; // PhoneWindow是Window的唯一实现类
final void attach(...) {
...
// 给mWindow赋值
mWindow = new PhoneWindow(this, window, activityConfigCallback); ...
}
- 在PhoneWindow中又直接将事件传递到DecorView的superDispatchTouchEvent
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用mDecor的方法
return mDecor.superDispatchTouchEvent(event);
}
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor; // 申明
public PhoneWindow(...) {
...
mDecor = (DecorView) preservedWindow.getDecorView(); // 初始化
...
}
- DecorView调用父类ViewGroup的superDispatchTouchEvent
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
// DecorView继承FrameLayout,而FrameLayout没有重写dispathToucheEvent,所以这里将会调用FrameLayout的父类ViewGroup的dispatchTouchEvent
return super.dispatchTouchEvent(event);
}
- ViewGroup部分源码,主要方法dispatchTouchEvent,dispatchTransformedTouchEvent
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 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);
// 每次ACTION_DOWN,都会通过resetTouchState,将mFirstTouchTarget和mGroupFlags重置
resetTouchState();
}
// Check for interception ====================================
final boolean intercepted; // 申明局部变量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;
}
// 是否拦截与actionMasked,mFirstTouchTarget,mGroupFlags,onInterceptTouchEvent()相关
/**
* 这一段代码来判断ViewGroup是否拦截该事件
* 先看第一个if,有两个条件:条件1如果是down事件需要进一步判断是否拦截,
条件2如果不是down事件,但mFirstTouchTarget不为空需要继续判断
* mFirstTouchTarget什么时候不为空?在down事件时,如果有子View对事件进行消费(返回true),则mFirstTouchTarget不为空
* 如果第一个if为false,则intercepted为false;该事件ViewGroup自己处理
(不一定是消费,而是进入dispatchTransformedTouchEvent方法,这个后面再详细解释)
* 如果第一个if为true,需要进一步判断,这里有一个mGroupFlags,
是子View要求ViewGroup是否拦截的标记(子View通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)来标记)
* 如果子View请求不拦截,即disallowIntercept为true,则intercepted = false;
* 如果子View请求ViewGroup拦截或者没有要求,那么disallowIntercept为false,
这里需要第三次判断,即通过ViewGroup的onInterceptTouchEvent来判断是否拦截
*
* 从事件流来看是否拦截:
* down事件:判断条件只有一个,就是onInterceptTouchEvent方法
* 后续事件:先down事件是否有子控件消费,即mFirstTouchTarget != nul,如果没有直接拦截;
如果被子控件消费,然后看子控件是否请求父类拦截,
即调用requestDisallowInterceptTouchEvent,如果要求不拦截,再去看onInterceptTouchEvent方法
*/
// Check for interception ====================================
...
// 判断是否被取消或拦截
if (!canceled && !intercepted) {
...
// 事件过滤,只处理down
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
// 理论上newTouchTarget为空,所以主要判断childrenCount,如果有就执行循环语句
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {
// isTransformedTouchPointInView判断事件发生坐标是否在子view上
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
}
}
...
// 这里dispatchTransformedTouchEvent的作用时判断child是否消费当前事件ev
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// addTouchTarget方法将对mFirstTouchTarget赋值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
...
break;
}
...
}
...
}
/**
* 这段代码是在不取消且不拦截的情况下,对down事件的处理
* 首先,条件必须时不取消不拦截,有子View
* 然后只处理down事件
* 如何处理,在子View集合中寻找符合条件的view,调用dispatchTransformedTouchEvent,看该View是否消费该事件;
* 如果有子View消费该事件,给mFirstTouchTarget赋值,并alreadyDispatchedToNewTouchTarget = true;
*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// mFirstTouchTarget比较好理解,在处理down事件时,没有被子View消费
// 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;
/** 如果当前时down事件,且事件被子View消费,这handler为true;没有被子View消费,不要执行到这里
* 如果不是down事件,alreadyDispatchedToNewTouchTarget为false
*/
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 进入dispatchTransformedTouchEvent方法,传入target.child
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; // 最后返回的handled表示事件是否被消费,被子View消费也算
}
/**
* 这段代码对事件行进处理
* down事件:如果子View处理了,这里便不再处理,直接handled = true,
如果子View没有处理,调用dispatchTransformedTouchEvent,View参数传入null
* 其他事件:如果子View没有处理,与down事件一样。
如果子View处理了,调用dispatchTransformedTouchEvent,View参数传入target.child
*/
// 在ViewGroup的dispatchTouchEvent中多处调用的dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
// 伪代码
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
// 伪代码
return handled;
}
/**
* dispatchTransformedTouchEvent方法中
* 如果传入View为null,那么调用ViewGroup的super.dispatchTouchEvent,即View的dispatchTouchEvent方法
* 如果传入View不为空,那么调用View本身的dispatchTouchEvent
* 如果View是ViewGroup的子类,那么又会进入上面重复的步骤,知道View是View的子类为止
* 所以,最终会执行View的dispatchTouchEvent
*/
- View部分源码,主要方法dispatchTouchEvent,onTouchEvent,performClick
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 如果设置了onTouchListener,且onTouch返回true
result = true;
}
if (!result && onTouchEvent(event)) {
// 如果onTouchEvent返回true
result = true;
}
...
return result;
}
/**
* 先判断是否设置了OnTouchListener
* 如果设置了OnTouchListener,看接口里的onTouch方法是否返回true;
如果返回true,事件被onTouch消费;如果返回false,事件没有被消费,进入onTouchEvent,事件是否被消费取决与onTouchEvent的返回值
* 如果没有设置OnTouchListener,直接进入onTouchEvent
事件是否被消费取决与onTouchEvent的返回值
* 总的来说,OnTouchListener中的onTouch方法优先于onTouchEvent方法
*/
public boolean onTouchEvent(MotionEvent event) {
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
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为DISABLED,clickable决定是否消费事件
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) { // 如果设置了mTouchDelegate,事件可以叫给mTouchDelegate消费
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
...
return true;
/**
* 如果clickable或者TOOLTIP,事件被消费
* TOOLTIP:Indicates this view can display a tool tip on hover or long press.
是表示这个View在悬停或者长按时能显示工具提示。例:EditText,再长按是可以出现复制粘贴工具提示
*/
}
// 再看if (clickable || (viewFlags & TOOLTIP) == TOOLTIP)这个条件内部,分别对down,move,up,cancel事件进行处理
// 先看down
case MotionEvent.ACTION_DOWN:
...
if (!clickable) {
// clickable为false,就开启一个长按延迟任务
checkForLongClick(0, x, y);
break;
}
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
...
break;
/**
* 如果是处在正滑动的父View中,也会开启一个CheckForTap任务,CheckForTap中会执行setPressed和开启一个长按延迟任务
* 如果父View没有滑动,则直接执行setPressed和开启一个长按延迟任务
*/
// 再看move
// Be lenient about moving outside of buttons
case MotionEvent.ACTION_MOVE:
...
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
...
break;
/**
* 如果移到View的外面,取消之前开启的延迟任务:CheckForTap任务和长按任务
*/
// 接着看cancal
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
...
// cancel取消了所有
// 最后看up
case MotionEvent.ACTION_UP:
...
removeLongPressCallback(); // 取消长按任务
...
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();
}
// post(mPerformClick)基本上都会返回true,mPerformClick任务会执行PerformClick()
// 这里就保证了performClick()一定会被调用;
if (!post(mPerformClick)) {
performClick();
}
}
...
removeTapCallback(); // 取消任务
...
break;
// up会调用performClick(),取消了长按任务和CheckForTap任务
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 如果设置了OnClickListener,会执行onClick方法,但不会应该事件消费的结果,该方法的返回值在onTouchEvent中并没有被使用
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
/**
* View中事件分发总结:
* 1. 是否设置了OnTouchListener,如果设置了,ouTouch返回true,则消费该事件
* 2. 如果设置了,onTouch返回false,或者没设置OnTouchListener,进入onTouchEvent
* 3. 在onTouchEvent中,返回结果基本与clickable一致
* OnTouchEvent.onTouch,onTouchEvent,OnClickListener.onClick的顺序:
onTouch->onTouchEvent->onClick
* onTouchEvent里面关于LongClick和Click事件处理
* down:标记PFLAG_PRESSED,并发起长按任务
* move:如果移出View的范围,取消标记PFLAG_PRESSED和长按任务。如果没有取消长按任务,但时间到达延迟时长会调用performLongClick,
如果设置长按监听事件,并修改mHasPerformedLongPress = true;在之后up时,不在执行performClick方法
* up:如果没有执行长按事件,也没有移出View的范围,会执行performClick,如果设置了单击监听事件会被执行。
* cancel:取消所有任务,状态
*/
流程总结:
- 首先每次触摸屏幕会长生一个事件流,包含一个down事件,若干个move事件,还有一个up事件
- Activity先调用自身的dispatchTouchEvent(在该方法中,执行getWindow().superDispatchTouchEvent(ev),getWindow返回Window的实现类PhoneWindow的对象)
- PhoneWindow.superDispatchTouchEvent
- DecorView.superDispatchTouchEvent(内部调用super.dispatchTouchEvent)
- ViewGroup.dispatchTouchEvent(DecorView继承FrameLayou,FrameLayout没有重写dispatchTouchEvent,所以直接调用父类ViewGroup的方法)
- 到进入ViewGroup.dispatchTouchEvent之前,对事件down,move,up的处理方式都一样的,之后要分事件讨论,这里有两个重要的属性mFirstTouchTarget,mGroupFlags
ViewGroup.dispatchTouchEvent-down事件
- 先将mFirstTouchTarget为null,取消mGroupFlags中FLAG_DISALLOW_INTERCEPT标记。然后根据VieGroup自身的onInterceptTouchEvent方法决定是否拦截该事件。分支1
- 分支1:如果拦截,会调用dispatchTransformedTouchEvent,View参数并传入null。执行super.dispatchTouchEvent,即ViewGroup的父类View的dispatchTouchEvent(View.dispatchTouchEvent后面单独分析)
- 分支1:如果不拦截,遍历所有子View,根据touch事件的x,y坐标,确定子View,并调用dispatchTransformedTouchEvent,View参数传入child(钢刚才找到的子View),执行child.dispatchTouchEvent。如果child是ViewGroup就再次上面的流程。这里直接看child不是ViewGroup的情况,view.dispatchTouchEvent的返回值。分支2
- 分支2:如果返回true,表示事件被消费,mFirstTouchTarget会被赋值
- 分支2:如果返回false,表示子View不消费该事件,继续看其他子View。
先写到这…
TODO:
- 总结
- 流程图
- 常见问题:例如Button的move事件移出控件范围,后续事件的传递,点击事件为什么不响应
参考:
- Android源码(API 27)
- Android进阶知识树——View、ViewGroup事件分发机制详解