一、前言
Android中事件分发与事件处理是一个老生常谈的问题了,自己在网上也看过很多文章,但是大部分人都只是抛出一些结论或是一些流程图或者干脆就是一些运行demo的截图等,对于这些结论和流程图是怎么来的,其实并没有解释太清楚,一段时间之后,这些结论也只是结论,至于为什么会有这样的结论脑子里一片空白,其实所有结论源码中都给了解释,今天从源码及结论两个方面进行代码梳理,方便加深记忆。
结论一:触摸事件从Activity分发下来,经过PhoneWindow、DecorView到达ViewGruop层面。
看源码之前先来简单复习一下Android页面层级结构:
Android页面层级结构
1. Activity
Activity作为四大组件之一,是平常使用度很高的一个类。如果你仔细观察会发现,这个类既不继承View又不继承ViewGroup,而是实现了Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener等一堆接口,它承担的更多的是监听的工作,称为控制器更合适,可以理解为用户触摸屏幕事件就是从该类下发下来的。
2. window
window:最顶级的窗口外观基类,只有一个实现类PhoneWindow。
3.decorView
当前窗口最顶层的View,是所有View的根节点,即当前Activity视图树的根节点,其父类为FrameLayout。
用一张图描述一下各个组件的涵盖关系:
下面我们就从源码角度看一下,触摸屏幕事件是如何从Activity一级一级分发下去的。
二、Activity中事件分发与处理
Activity.java
//Activity中事件分发
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//Activity中事件处理
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
复制代码
在Activity的dispatchTouchEvent()中,如果getWindow().superDispatchTouchEvent(ev)返回true,方法到此就结束了,如果返回false,则会调用Activity的onTouchEvent()。在Activity的onTouchEvent()中,如果window在触摸屏幕时应该关闭,则Activity的onTouchEvent()返回true,并且finish,否则返回fasle。
PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
复制代码
在PhoneWindow中,其superDispatchTouchEvent()内部调用了DecorView的superDispatchTouchEvent()
Decorview.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
复制代码
在DecorView中,superDispatchTouchEvent()方法调用了其父类FrameLayout的dispatchTouchEvent(),FrameLayout中没有提供该方法,该方法的实际提供者是ViewGruop,到此我们知道了MotionEvent如何从Activity一步步传递到了ViewGruop中。
我们在看下其它比较常见的结论:
结论二:子View不消费down事件(onTouch()返回false或onTouchEvent()返回false),move、up事件不会再往该view分发,父View触发onTouch或OnTouchEvent事件。
结论三:子View消费down事件,后续move、up事件会直接发向该view(不考虑拦截)。
结论四:子View消费down事件,move、up不被拦截但是不消费move、up事件时,不会触发父View onTouchEvent()
结论五:子View消费down事件,move、up被拦截会收到来自父View的cancel事件,move、up会发送给父View,触发父View onTouchEvent()
下面从源码中找一下,为什么会有这样的结论。
三、ViewGruop中事件分发与处理
ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......省略部分代码
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
//每次接收到down事件,清除所有状态,最重要的是把mFirstTouchTarget置为null
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.
//校验是否拦截touch事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {子view允许父view拦截
intercepted = onInterceptTouchEvent(ev);//是否拦截由onInterceptTouchEvent(ev)方法决定
ev.setAction(action); // restore action in case it was changed
} else {//requestDisallowInterceptTouchEvent()设置为true,子view禁止父view拦截
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;
}
......省略部分代码
// 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 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;
//down事件下发逻辑
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;
//倒序遍历ViewGroup的子view,如果view所在区域处于触摸点坐标区域,
//则调用dispatchTransformedTouchEvent()方法
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 (!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;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//知识点二,找到了要处理touch事件的view,newTouchTarget、alreadyDispatchedToNewTouchTarget赋值
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);//找到要分发的view,mFirstTouchTarget设置值
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();
}
......省略部分代码
}
}
// 分发给触摸view
if (mFirstTouchTarget == null) {
//没有view要处理该touch事件,viewGroup自己处理,注意第三个参数为null
//知识点三
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//move、up事件下发逻辑
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;//上面执行过down事件,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();//如果是up、cancel,重置触摸状态,mFirstTouchTarget设为null
} 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;
}
复制代码
源码中标识知识点一:判定条件意味着当前事件不是down事件,且viewGroup内没有子view要处理事件,intercepted变量设置为true,对move、up等事件进行拦截,这也就是结论二的原因。
知识点二、三、四调用了同一个方法:dispatchTransformedTouchEvent(),先看一下这个方法,在分析二、三、四处的作用有何不同。
ViewGroup.java
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) {//事件类型为cancel
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);//调用父类View的dispatchTouchEvent()
} else {
handled = child.dispatchTouchEvent(event);//调用child.dispatchTouchEvent()、child可能为view也可能为viewGroup
}
event.setAction(oldAction);
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
......省略部分代码
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);//调用父类View的dispatchTouchEvent()
} 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);//调用child.dispatchTouchEvent()、child可能为view也可能为viewGroup
}
// Done.
transformedEvent.recycle();
return handled;
}
复制代码
通过查阅源码,我们发现dispatchTransformedTouchEvent()的返回值分两种情况
- child为null,会调用ViewGroup的父类的dispatchTouchEvent()方法,即View的dispatchTouchEvent()
- child不为null,将触摸点坐标变换至子view坐标系,并且调用子View的dispatchTouchEvent()
知识点二如果返回true,则证明child即为消费down事件的子View,调用addTouchTarget()为mFirstTouchTarget赋值,后续的move、up事件会走知识点四,如果返回false,则证明不存在消费down事件的子View,后续的move、up会走知识点三,也就是把ViewGroup视为View,调用View的dispatchTouchEvent()。这也正是结论三的原因。
四、View中事件分发与处理
View.java
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
......省略部分代码
boolean result = false;
......省略部分代码
if (onFilterTouchEventForSecurity(event)) {
......省略部分代码
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//如果View设置了OnTouchListener()并且onTouch()回调返回true
result = true;
}
if (!result && onTouchEvent(event)) {//如果onTouchEvent()返回true
result = true;
}
}
......省略部分代码
return result;
}
复制代码
dispatchTouchEvent()中会先验证View是否设置过触摸监听调用SetOnTouchListener()且当前view是否已经启用且其onTouch()是否true。
- 如果前面条件均满足,则result设置为true,跳过onTouchEvent(event)的执行,结束事件分发
- 否则,调用onTouchEvent(event),如果onTouchEvent(event)返回true,result设置为true,结束事件分发。
View.java
public boolean onTouchEvent(MotionEvent event) {
......
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
.....
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
return false;
}
复制代码
只要view是可点击或悬停、长摁时提示tooltip,onTouchEvent(MotionEvent event)一定返回true,否则返回false。
如果down返回true,move或up返回false,则down后面的事件会因为mFirstTouchTarget !=null进入知识点四,导致父View无法执行知识点二,这是结论四的原因。
如果down返回true,down后续事件被父View拦截,会导致知识点四参数二为true,mFirstTouchTarget重置为null,
即子view接收到cancel事件,父View触发知识点二调用onTouch或onTouchEvent()。这是结论五的原因。
但是知识点三的代码已经走过了,即使把mFirstTouchTarget重置为null,知识点二的代码也不会走了,那么系统是在何时又触发了知识点二的执行呢?答案是:下个事件到来时,如果父View拦截的是move事件,等到第二个move到来时会触发父View的move、up;如果父View拦截的是up事件,则父View也不会执行onTouchEvent(),因为只有一个up事件。
附上验证截图兩张:
可以看到,只拦截up事件的父View,不会触发自身onTouchEvent()的执行。
可以看到,拦截move事件的父View,会触发自身onTouchEvent()的执行。
五、调用关系梳理
在不进行事件拦截、不调用SetOnTouchListener()、没有多个父布局的前提下
触摸事件从Activity下发至View流程:
上图中,调用child的dispatchTouchEvent()时,如果child属于ViewGroup类型,依然会调用ViewGruop的dispatchEvent()方法,直至最后一层view视图,才会调用view.dispatchEvent()。